feat(core): allow dependsOn to accept a single project dependency (#16100)

This commit is contained in:
Craigory Coppola 2023-04-10 13:12:16 -04:00 committed by GitHub
parent 7989facc85
commit f2f6e356a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 491 additions and 126 deletions

View File

@ -276,6 +276,9 @@ result in `mylib`'s dependencies being built as well.
You can also express the same configuration using:
{% tabs %}
{% tab label="Version < 16" %}
```json
"build": {
"dependsOn": [{ "projects": "dependencies", "target": "build" }]
@ -285,8 +288,26 @@ You can also express the same configuration using:
}
```
{% /tab %}
{% tab label="Version 16+" %}
```json
"build": {
"dependsOn": [{ "projects": "{dependencies}", "target": "build" }]
},
"test": {
"dependsOn": [{ "projects": "{self}", "target": "build" }]
}
```
{% /tab %}
{% /tabs %}
With the expanded syntax, you also have a third option available to configure how to handle the params passed to the target. You can either forward them or you can ignore them (default).
{% tabs %}
{% tab label="Version < 16" %}
```json
"build": {
// forward params passed to this target to the dependency targets
@ -302,8 +323,32 @@ With the expanded syntax, you also have a third option available to configure ho
}
```
{% /tab %}
{% tab label="Version 16+" %}
```json
"build": {
// forward params passed to this target to the dependency targets
"dependsOn": [{ "projects": "{dependencies}", "target": "build", "params": "forward" }]
},
"test": {
// ignore params passed to this target, won't be forwarded to the dependency targets
"dependsOn": [{ "projects": "{dependencies}", "target": "build", "params": "ignore" }]
}
"lint": {
// ignore params passed to this target, won't be forwarded to the dependency targets
"dependsOn": [{ "projects": "{dependencies}", "target": "build" }]
}
```
{% /tab %}
{% /tabs %}
Obviously this also works when defining a relation for the target of the project itself using `"projects": "self"`:
{% tabs %}
{% tab label="Version < 16" %}
```json
"build": {
// forward params passed to this target to the project target
@ -311,6 +356,28 @@ Obviously this also works when defining a relation for the target of the project
}
```
{% /tab %}
{% tab label="Version 16+" %}
```json
"build": {
// forward params passed to this target to the project target
"dependsOn": [{ "projects": "{self}", "target": "pre-build", "params": "forward" }]
}
```
{% /tab %}
{% /tabs %}
Additionally, when using the expanded object syntax, you can specify individual projects in version 16 or greater.
```json
"build": {
// Run is-even:pre-build and is-odd:pre-build before this target
"dependsOn": [{ "projects": ["is-even", "is-odd"], "target": "pre-build" }]
}
```
This configuration is usually not needed. Nx comes with reasonable defaults (imported in `nx.json`) which implement the
configuration above.

View File

@ -59,6 +59,12 @@
"version": "16.0.0-beta.0",
"description": "Remove @nrwl/cli.",
"implementation": "./src/migrations/update-16-0-0/remove-nrwl-cli"
},
"16.0.0-tokens-for-depends-on": {
"cli": "nx",
"version": "16.0.0-beta.0",
"description": "Replace `dependsOn.projects` with {self} or {dependencies} tokens so that it matches the new expected formatting.",
"implementation": "./src/migrations/update-16-0-0/update-depends-on-to-tokens"
}
}
}

View File

@ -247,9 +247,19 @@
"type": "object",
"properties": {
"projects": {
"type": "string",
"description": "The projects that the targets belong to.",
"enum": ["self", "dependencies"]
"oneOf": [
{
"type": "string",
"description": "{self}, {dependencies}, or a project name."
},
{
"type": "array",
"description": "An array of project specifiers: {self}, {dependencies}, or a project name.",
"items": {
"type": "string"
}
}
]
},
"target": {
"type": "string",

View File

@ -55,9 +55,19 @@
"type": "object",
"properties": {
"projects": {
"type": "string",
"description": "The projects that the targets belong to.",
"enum": ["self", "dependencies"]
"oneOf": [
{
"type": "string",
"description": "{self}, {dependencies}, or a project name."
},
{
"type": "array",
"description": "An array of project specifiers: {self}, {dependencies}, or a project name.",
"items": {
"type": "string"
}
}
]
},
"target": {
"type": "string",

View File

@ -65,9 +65,19 @@
"type": "object",
"properties": {
"projects": {
"type": "string",
"description": "The projects that the targets belong to.",
"enum": ["self", "dependencies"]
"oneOf": [
{
"type": "string",
"description": "{self}, {dependencies}, or a project name."
},
{
"type": "array",
"description": "An array of project specifiers: {self}, {dependencies}, or a project name.",
"items": {
"type": "string"
}
}
]
},
"target": {
"type": "string",

View File

@ -102,12 +102,15 @@ export interface ProjectConfiguration {
export interface TargetDependencyConfig {
/**
* This the projects that the targets belong to
* A list of projects that have `target`. Supports project names or two special values:
*
* 'self': This target depends on another target of the same project
* 'deps': This target depends on targets of the projects of it's deps.
* - '{self}': This target depends on another target of the same project
* - '{dependencies}': This target depends on targets of the projects of it's deps.
*
* The special values {self}/{dependencies} should be preferred - they prevent cases where a project
* that needs to be built is missed.
*/
projects: 'self' | 'dependencies';
projects: string[] | string;
/**
* The name of the target

View File

@ -0,0 +1,104 @@
import {
addProjectConfiguration,
getProjects,
readNxJson,
readProjectConfiguration,
} from '../../generators/utils/project-configuration';
import { Tree } from '../../generators/tree';
import update from './update-depends-on-to-tokens';
import { updateJson, writeJson } from 'nx/src/devkit-exports';
import { createTreeWithEmptyWorkspace } from '../../generators/testing-utils/create-tree-with-empty-workspace';
describe('update-depends-on-to-tokens', () => {
it('should update nx.json', async () => {
const tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'nx.json', (json) => {
json.targetDefaults = {
build: {
dependsOn: [
{
projects: 'self',
},
],
},
test: {
dependsOn: [
{
projects: 'dependencies',
},
],
},
other: {
dependsOn: ['^deps'],
},
};
return json;
});
await update(tree);
const nxJson = readNxJson(tree);
const build = nxJson.targetDefaults.build.dependsOn[0] as any;
const test = nxJson.targetDefaults.test.dependsOn[0] as any;
expect(build.projects).toEqual('{self}');
expect(test.projects).toEqual('{dependencies}');
expect(nxJson.targetDefaults.other.dependsOn).toEqual(['^deps']);
});
it('should update project configurations', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
build: {
dependsOn: [
{
projects: 'self',
target: 'build',
},
],
},
test: {
dependsOn: [
{
projects: 'dependencies',
target: 'test',
},
],
},
other: {
dependsOn: ['^deps'],
},
},
});
await update(tree);
const project = readProjectConfiguration(tree, 'proj1');
const build = project.targets.build.dependsOn[0] as any;
const test = project.targets.test.dependsOn[0] as any;
expect(build.projects).toEqual('{self}');
expect(test.projects).toEqual('{dependencies}');
expect(project.targets.other.dependsOn).toEqual(['^deps']);
});
it('should not throw on nulls', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
});
addProjectConfiguration(tree, 'proj2', {
root: 'proj2',
targets: {
build: {},
},
});
writeJson(tree, 'nx.json', {});
let promise = update(tree);
await expect(promise).resolves.toBeUndefined();
writeJson(tree, 'nx.json', {
targetDefaults: {
build: {},
},
});
promise = update(tree);
await expect(promise).resolves.toBeUndefined();
});
});

View File

@ -0,0 +1,56 @@
import {
getProjects,
readNxJson,
updateNxJson,
updateProjectConfiguration,
} from '../../generators/utils/project-configuration';
import { Tree } from '../../generators/tree';
export default async function (tree: Tree) {
updateDependsOnInsideNxJson(tree);
const projectsConfigurations = getProjects(tree);
for (const [projectName, projectConfiguration] of projectsConfigurations) {
let projectChanged = false;
for (const [targetName, targetConfiguration] of Object.entries(
projectConfiguration.targets ?? {}
)) {
for (const dependency of targetConfiguration.dependsOn ?? []) {
if (typeof dependency !== 'string') {
if (dependency.projects === 'self') {
dependency.projects = '{self}';
projectChanged = true;
} else if (dependency.projects === 'dependencies') {
dependency.projects = '{dependencies}';
projectChanged = true;
}
}
}
}
if (projectChanged) {
updateProjectConfiguration(tree, projectName, projectConfiguration);
}
}
}
function updateDependsOnInsideNxJson(tree: Tree) {
const nxJson = readNxJson(tree);
let nxJsonChanged = false;
for (const [target, defaults] of Object.entries(
nxJson?.targetDefaults ?? {}
)) {
for (const dependency of defaults.dependsOn ?? []) {
if (typeof dependency !== 'string') {
if (dependency.projects === 'self') {
dependency.projects = '{self}';
nxJsonChanged = true;
} else if (dependency.projects === 'dependencies') {
dependency.projects = '{dependencies}';
nxJsonChanged = true;
}
}
}
}
if (nxJsonChanged) {
updateNxJson(tree, nxJson);
}
}

View File

@ -17,17 +17,24 @@ describe('createTaskGraph', () => {
prebuild: {
executor: 'nx:run-commands',
},
prebuild2: {
executor: 'nx:run-commands',
},
build: {
executor: 'nx:run-commands',
dependsOn: [
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'build',
},
{
projects: 'self',
projects: '{self}',
target: 'prebuild',
},
{
projects: 'app1',
target: 'prebuild2',
},
],
},
test: {
@ -166,7 +173,7 @@ describe('createTaskGraph', () => {
},
dependsOn: [
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'build',
},
],
@ -189,7 +196,7 @@ describe('createTaskGraph', () => {
defaultConfiguration: 'libDefault',
dependsOn: [
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'build',
},
],
@ -330,7 +337,7 @@ describe('createTaskGraph', () => {
executor: 'my-executor',
dependsOn: [
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'build',
},
],
@ -501,11 +508,11 @@ describe('createTaskGraph', () => {
executor: 'nx:run-commands',
dependsOn: [
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'build',
params: 'forward',
},
{ projects: 'self', target: 'prebuild', params: 'forward' },
{ projects: '{self}', target: 'prebuild', params: 'forward' },
],
},
test: {
@ -528,7 +535,7 @@ describe('createTaskGraph', () => {
executor: 'nx:run-commands',
dependsOn: [
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'build',
params: 'ignore',
},
@ -640,7 +647,7 @@ describe('createTaskGraph', () => {
);
// prebuild should also be in here
expect(taskGraph).toEqual({
roots: ['lib1:build', 'app1:prebuild'],
roots: ['lib1:build', 'app1:prebuild', 'app1:prebuild2'],
tasks: {
'app1:build': {
id: 'app1:build',
@ -664,6 +671,17 @@ describe('createTaskGraph', () => {
},
projectRoot: 'app1-root',
},
'app1:prebuild2': {
id: 'app1:prebuild2',
target: {
project: 'app1',
target: 'prebuild2',
},
overrides: {
__overrides_unparsed__: [],
},
projectRoot: 'app1-root',
},
'lib1:build': {
id: 'lib1:build',
target: {
@ -677,8 +695,9 @@ describe('createTaskGraph', () => {
},
},
dependencies: {
'app1:build': ['lib1:build', 'app1:prebuild'],
'app1:build': ['lib1:build', 'app1:prebuild', 'app1:prebuild2'],
'app1:prebuild': [],
'app1:prebuild2': [],
'lib1:build': [],
},
});
@ -697,7 +716,7 @@ describe('createTaskGraph', () => {
);
// prebuild should also be in here
expect(taskGraph).toEqual({
roots: ['app1:prebuild', 'lib1:build'],
roots: ['app1:prebuild', 'lib1:build', 'app1:prebuild2'],
tasks: {
'app1:build': {
id: 'app1:build',
@ -721,6 +740,17 @@ describe('createTaskGraph', () => {
},
projectRoot: 'app1-root',
},
'app1:prebuild2': {
id: 'app1:prebuild2',
target: {
project: 'app1',
target: 'prebuild2',
},
overrides: {
__overrides_unparsed__: [],
},
projectRoot: 'app1-root',
},
'lib1:build': {
id: 'lib1:build',
target: {
@ -734,8 +764,9 @@ describe('createTaskGraph', () => {
},
},
dependencies: {
'app1:build': ['lib1:build', 'app1:prebuild'],
'app1:build': ['lib1:build', 'app1:prebuild', 'app1:prebuild2'],
'app1:prebuild': [],
'app1:prebuild2': [],
'lib1:build': [],
},
});
@ -813,7 +844,7 @@ describe('createTaskGraph', () => {
{
build: [
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'build',
},
],
@ -974,9 +1005,9 @@ describe('createTaskGraph', () => {
{
build: ['^build'],
apply: [
{ projects: 'dependencies', target: 'build' },
{ projects: '{dependencies}', target: 'build' },
{
projects: 'dependencies',
projects: '{dependencies}',
target: 'apply',
params: 'forward',
},
@ -1052,11 +1083,11 @@ describe('createTaskGraph', () => {
targets: {
build: {
executor: 'nx:run-commands',
dependsOn: [{ target: 'test', projects: 'self' }],
dependsOn: [{ target: 'test', projects: '{self}' }],
},
test: {
executor: 'nx:run-commands',
dependsOn: [{ target: 'build', projects: 'self' }],
dependsOn: [{ target: 'build', projects: '{self}' }],
},
},
},
@ -1161,7 +1192,7 @@ describe('createTaskGraph', () => {
const taskGraph = createTaskGraph(
projectGraph,
{
build: [{ target: 'build', projects: 'dependencies' }],
build: [{ target: 'build', projects: '{dependencies}' }],
},
['app1'],
['build'],
@ -1252,7 +1283,7 @@ describe('createTaskGraph', () => {
const taskGraph = createTaskGraph(
projectGraph,
{
build: [{ target: 'build', projects: 'dependencies' }],
build: [{ target: 'build', projects: '{dependencies}' }],
},
['app1'],
['build'],
@ -1307,8 +1338,8 @@ describe('createTaskGraph', () => {
build: {
executor: 'nx:run-commands',
dependsOn: [
{ target: 'prebuild', projects: 'self' },
{ target: 'build', projects: 'dependencies' },
{ target: 'prebuild', projects: '{self}' },
{ target: 'build', projects: '{dependencies}' },
],
},
prebuild: {

View File

@ -6,6 +6,7 @@ import {
} from '../utils/project-graph-utils';
import { Task, TaskGraph } from '../config/task-graph';
import { TargetDependencies } from '../config/nx-json';
import { TargetDependencyConfig } from '../devkit-exports';
export class ProcessTasks {
private readonly seen = new Set<string>();
@ -104,92 +105,150 @@ export class ProcessTasks {
dependencyConfig.params === 'forward'
? overrides
: { __overrides_unparsed__: [] };
const targetProjectSpecifiers =
typeof dependencyConfig.projects === 'string'
? [dependencyConfig.projects]
: dependencyConfig.projects;
for (const projectSpecifier of targetProjectSpecifiers) {
// Lerna uses `dependencies` in `prepNxOptions`, so we need to maintain
// support for it until lerna can be updated to use the new tokens.
// TODO(@agentender): Remove this part in v17
if (
projectSpecifier === '{dependencies}' ||
(projectSpecifier === 'dependencies' &&
!this.projectGraph.nodes[projectSpecifier])
) {
this.processTasksForDependencies(
projectUsedToDeriveDependencies,
dependencyConfig,
configuration,
task,
taskOverrides,
overrides
);
} else {
// Since we need to maintain support for dependencies, it is more coherent
// that we also support self.
// TODO(@agentender): Remove this part in v17
const projectName =
projectSpecifier === '{self}' ||
(projectSpecifier === 'self' &&
!this.projectGraph.nodes[projectSpecifier])
? task.target.project
: projectSpecifier;
if (dependencyConfig.projects === 'dependencies') {
for (const dep of this.projectGraph.dependencies[
projectUsedToDeriveDependencies
]) {
const depProject = this.projectGraph.nodes[
dep.target
] as ProjectGraphProjectNode;
this.processTasksForSingleProject(
task,
projectName,
dependencyConfig,
configuration,
taskOverrides,
overrides
);
}
}
}
}
// this is to handle external dependencies
if (!depProject) continue;
private processTasksForSingleProject(
task: Task,
projectName: string,
dependencyConfig: TargetDependencyConfig,
configuration: string,
taskOverrides: Object | { __overrides_unparsed__: any[] },
overrides: Object
) {
const selfProject = this.projectGraph.nodes[
projectName
] as ProjectGraphProjectNode;
if (projectHasTarget(depProject, dependencyConfig.target)) {
const resolvedConfiguration = this.resolveConfiguration(
depProject,
dependencyConfig.target,
configuration
);
const depTargetId = this.getId(
depProject.name,
dependencyConfig.target,
resolvedConfiguration
);
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,
taskOverrides
);
this.tasks[selfTaskId] = newTask;
this.dependencies[selfTaskId] = [];
this.processTask(
newTask,
newTask.target.project,
configuration,
overrides
);
}
}
}
if (task.id !== depTargetId) {
this.dependencies[task.id].push(depTargetId);
}
if (!this.tasks[depTargetId]) {
const newTask = this.createTask(
depTargetId,
depProject,
dependencyConfig.target,
resolvedConfiguration,
taskOverrides
);
this.tasks[depTargetId] = newTask;
this.dependencies[depTargetId] = [];
private processTasksForDependencies(
projectUsedToDeriveDependencies: string,
dependencyConfig: TargetDependencyConfig,
configuration: string,
task: Task,
taskOverrides: Object | { __overrides_unparsed__: any[] },
overrides: Object
) {
for (const dep of this.projectGraph.dependencies[
projectUsedToDeriveDependencies
]) {
const depProject = this.projectGraph.nodes[
dep.target
] as ProjectGraphProjectNode;
this.processTask(
newTask,
newTask.target.project,
configuration,
overrides
);
}
} else {
this.processTask(task, depProject.name, configuration, overrides);
}
// 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,
taskOverrides
);
this.tasks[depTargetId] = newTask;
this.dependencies[depTargetId] = [];
this.processTask(
newTask,
newTask.target.project,
configuration,
overrides
);
}
} 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,
taskOverrides
);
this.tasks[selfTaskId] = newTask;
this.dependencies[selfTaskId] = [];
this.processTask(
newTask,
newTask.target.project,
configuration,
overrides
);
}
}
this.processTask(task, depProject.name, configuration, overrides);
}
}
}

View File

@ -32,17 +32,26 @@ export function getDependencyConfigs(
[]
);
for (const dependencyConfig of dependencyConfigs) {
if (
dependencyConfig.projects !== 'dependencies' &&
dependencyConfig.projects !== 'self'
) {
output.error({
title: `dependsOn is improperly configured for ${project}:${target}`,
bodyLines: [
`dependsOn.projects is "${dependencyConfig.projects}" but should be "self" or "dependencies"`,
],
});
process.exit(1);
const specifiers =
typeof dependencyConfig.projects === 'string'
? [dependencyConfig.projects]
: dependencyConfig.projects;
for (const specifier of specifiers) {
if (
!(specifier in projectGraph.nodes) &&
// Todo(@agentender): Remove the non-token forms of these for v17
!['{self}', '{dependencies}', 'self', 'dependencies'].includes(
specifier
)
) {
output.error({
title: `dependsOn is improperly configured for ${project}:${target}`,
bodyLines: [
`${specifier} in dependsOn.projects is invalid. It should be "{self}", "{dependencies}", or a project name.`,
],
});
process.exit(1);
}
}
}
return dependencyConfigs;
@ -54,9 +63,9 @@ function expandDependencyConfigSyntaxSugar(
return deps.map((d) => {
if (typeof d === 'string') {
if (d.startsWith('^')) {
return { projects: 'dependencies', target: d.substring(1) };
return { projects: '{dependencies}', target: d.substring(1) };
} else {
return { projects: 'self', target: d };
return { projects: '{self}', target: d };
}
} else {
return d;