Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): name collisions during project inference should not error out if corrected by a project.json's name #18665

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions docs/generated/devkit/CreateNodesContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
### Properties

- [nxJsonConfiguration](../../devkit/documents/CreateNodesContext#nxjsonconfiguration)
- [projectsConfigurations](../../devkit/documents/CreateNodesContext#projectsconfigurations)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have access to these any more in the area where we call createNodes, and doing so could incur a significant perf hit. I don't think its a big deal to drop this, since the API isn't documented yet and is still experimental.

- [workspaceRoot](../../devkit/documents/CreateNodesContext#workspaceroot)

## Properties
Expand All @@ -18,12 +17,6 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)

---

### projectsConfigurations

• `Readonly` **projectsConfigurations**: `Record`<`string`, [`ProjectConfiguration`](../../devkit/documents/ProjectConfiguration)\>

---

### workspaceRoot

• `Readonly` **workspaceRoot**: `string`
24 changes: 11 additions & 13 deletions packages/nx/src/generators/utils/project-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
ProjectConfiguration,
ProjectsConfigurations,
} from '../../config/workspace-json-project-json';
import { mergeProjectConfigurationIntoProjectsConfigurations } from '../../project-graph/utils/project-configuration-utils';
import {
mergeProjectConfigurationIntoRootMap,
readProjectConfigurationsFromRootMap,
} from '../../project-graph/utils/project-configuration-utils';
import { retrieveProjectConfigurationPathsWithoutPluginInference } from '../../project-graph/utils/retrieve-workspace-files';
import { output } from '../../utils/output';
import { PackageJson } from '../../utils/package-json';
Expand Down Expand Up @@ -207,26 +210,20 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
(r) => deletedFiles.indexOf(r) === -1
);

const rootMap: Map<string, string> = new Map();
return projectFiles.reduce((projects, projectFile) => {
const rootMap: Map<string, ProjectConfiguration> = new Map();
for (const projectFile of projectFiles) {
if (basename(projectFile) === 'project.json') {
const json = readJson(tree, projectFile);
const config = buildProjectFromProjectJson(json, projectFile);
mergeProjectConfigurationIntoProjectsConfigurations(
projects,
rootMap,
config,
projectFile
);
mergeProjectConfigurationIntoRootMap(rootMap, config, projectFile);
} else {
const packageJson = readJson<PackageJson>(tree, projectFile);
const config = buildProjectConfigurationFromPackageJson(
packageJson,
projectFile,
readNxJson(tree)
);
mergeProjectConfigurationIntoProjectsConfigurations(
projects,
mergeProjectConfigurationIntoRootMap(
rootMap,
// Inferred targets, tags, etc don't show up when running generators
// This is to help avoid running into issues when trying to update the workspace
Expand All @@ -237,8 +234,9 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
projectFile
);
}
return projects;
}, {});
}

return readProjectConfigurationsFromRootMap(rootMap);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { TargetConfiguration } from '../../config/workspace-json-project-json';
import {
ProjectConfiguration,
TargetConfiguration,
} from '../../config/workspace-json-project-json';
import {
mergeProjectConfigurationIntoRootMap,
mergeTargetConfigurations,
readProjectConfigurationsFromRootMap,
readTargetDefaultsForTarget,
} from './project-configuration-utils';

Expand Down Expand Up @@ -352,3 +357,146 @@ describe('target defaults', () => {
});
});
});

describe('mergeProjectConfigurationIntoRootMap', () => {
it('should merge targets from different configurations', () => {
const rootMap = new RootMapBuilder()
.addProject({
root: 'libs/lib-a',
name: 'lib-a',
targets: {
echo: {
command: 'echo lib-a',
},
},
})
.getRootMap();
mergeProjectConfigurationIntoRootMap(
rootMap,
{
root: 'libs/lib-a',
name: 'lib-a',
targets: {
build: {
command: 'tsc',
},
},
},
'inferred-project-config-file.ts'
);
expect(rootMap.get('libs/lib-a')).toMatchInlineSnapshot(`
{
"name": "lib-a",
"root": "libs/lib-a",
"targets": {
"build": {
"command": "tsc",
},
"echo": {
"command": "echo lib-a",
},
},
}
`);
});

it("shouldn't overwrite project name, unless merging project from project.json", () => {
const rootMap = new RootMapBuilder()
.addProject({
name: 'bad-name',
root: 'libs/lib-a',
})
.getRootMap();
mergeProjectConfigurationIntoRootMap(
rootMap,
{
name: 'other-bad-name',
root: 'libs/lib-a',
},
'libs/lib-a/package.json'
);
expect(rootMap.get('libs/lib-a').name).toEqual('bad-name');
mergeProjectConfigurationIntoRootMap(
rootMap,
{
name: 'lib-a',
root: 'libs/lib-a',
},
'libs/lib-a/project.json'
);
expect(rootMap.get('libs/lib-a').name).toEqual('lib-a');
});
});

describe('readProjectsConfigurationsFromRootMap', () => {
it('should error if multiple roots point to the same project', () => {
const rootMap = new RootMapBuilder()
.addProject({
name: 'lib',
root: 'apps/lib-a',
})
.addProject({
name: 'lib',
root: 'apps/lib-b',
})
.getRootMap();

expect(() => {
readProjectConfigurationsFromRootMap(rootMap);
}).toThrowErrorMatchingInlineSnapshot(`
"The following projects are defined in multiple locations:
- lib:
- apps/lib-a
- apps/lib-b

To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name."
`);
});

it('should read root map into standard projects configurations form', () => {
const rootMap = new RootMapBuilder()
.addProject({
name: 'lib-a',
root: 'libs/a',
})
.addProject({
name: 'lib-b',
root: 'libs/b',
})
.addProject({
name: 'lib-shared-b',
root: 'libs/shared/b',
})
.getRootMap();
expect(readProjectConfigurationsFromRootMap(rootMap))
.toMatchInlineSnapshot(`
{
"lib-a": {
"name": "lib-a",
"root": "libs/a",
},
"lib-b": {
"name": "lib-b",
"root": "libs/b",
},
"lib-shared-b": {
"name": "lib-shared-b",
"root": "libs/shared/b",
},
}
`);
});
});

class RootMapBuilder {
private rootMap: Map<string, ProjectConfiguration> = new Map();

addProject(p: ProjectConfiguration) {
this.rootMap.set(p.root, p);
return this;
}

getRootMap() {
return this.rootMap;
}
}
102 changes: 64 additions & 38 deletions packages/nx/src/project-graph/utils/project-configuration-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,42 @@ import {
ProjectConfiguration,
TargetConfiguration,
} from '../../config/workspace-json-project-json';
import { readJsonFile } from '../../utils/fileutils';
import { NX_PREFIX } from '../../utils/logger';
import { NxPluginV2 } from '../../utils/nx-plugin';
import { workspaceRoot } from '../../utils/workspace-root';

import minimatch = require('minimatch');
export function mergeProjectConfigurationIntoProjectsConfigurations(
// projectName -> ProjectConfiguration
existingProjects: Record<string, ProjectConfiguration>,
// projectRoot -> projectName
existingProjectRootMap: Map<string, string>,

export function mergeProjectConfigurationIntoRootMap(
projectRootMap: Map<string, ProjectConfiguration>,
project: ProjectConfiguration,
// project.json is a special case, so we need to detect it.
file: string
): void {
let matchingProjectName = existingProjectRootMap.get(project.root);
const matchingProject = projectRootMap.get(project.root);

if (!matchingProjectName) {
existingProjects[project.name] = project;
existingProjectRootMap.set(project.root, project.name);
if (!matchingProject) {
projectRootMap.set(project.root, project);
return;
// There are some special cases for handling project.json - mainly
// that it should override any name the project already has.
} else if (
project.name &&
project.name !== matchingProjectName &&
project.name !== matchingProject.name &&
basename(file) === 'project.json'
) {
// Copy config to new name
existingProjects[project.name] = existingProjects[matchingProjectName];
// Update name in project config
existingProjects[project.name].name = project.name;
// Update root map to point to new name
existingProjectRootMap[project.root] = project.name;
// Remove entry for old name
delete existingProjects[matchingProjectName];
// Update name that config should be merged to
matchingProjectName = project.name;
// `name` inside project.json overrides any names from
// inference plugins
matchingProject.name = project.name;
}

const matchingProject = existingProjects[matchingProjectName];

// This handles top level properties that are overwritten. `srcRoot`, `projectType`, or fields that Nx doesn't know about.
// This handles top level properties that are overwritten.
// e.g. `srcRoot`, `projectType`, or other fields that shouldn't be extended
// Note: `name` is set specifically here to keep it from changing. The name is
// always determined by the first inference plugin to ID a project, unless it has
// a project.json in which case it was already updated above.
const updatedProjectConfiguration = {
...matchingProject,
...project,
name: matchingProjectName,
name: matchingProject.name,
};

// The next blocks handle properties that should be themselves merged (e.g. targets, tags, and implicit dependencies)
Expand Down Expand Up @@ -83,11 +72,10 @@ export function mergeProjectConfigurationIntoProjectsConfigurations(
};
}

if (updatedProjectConfiguration.name !== matchingProject.name) {
delete existingProjects[matchingProject.name];
}
existingProjects[updatedProjectConfiguration.name] =
updatedProjectConfiguration;
projectRootMap.set(
updatedProjectConfiguration.root,
updatedProjectConfiguration
);
}

export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
Expand All @@ -99,8 +87,7 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
projects: Record<string, ProjectConfiguration>;
externalNodes: Record<string, ProjectGraphExternalNode>;
} {
const projectRootMap: Map<string, string> = new Map();
const projects: Record<string, ProjectConfiguration> = {};
const projectRootMap: Map<string, ProjectConfiguration> = new Map();
const externalNodes: Record<string, ProjectGraphExternalNode> = {};

// We push the nx core node builder onto the end, s.t. it overwrites any user specified behavior
Expand All @@ -119,13 +106,11 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
if (minimatch(file, pattern)) {
const { projects: projectNodes, externalNodes: pluginExternalNodes } =
configurationConstructor(file, {
projectsConfigurations: projects,
nxJsonConfiguration: nxJson,
workspaceRoot: root,
});
for (const node in projectNodes) {
mergeProjectConfigurationIntoProjectsConfigurations(
projects,
mergeProjectConfigurationIntoRootMap(
projectRootMap,
projectNodes[node],
file
Expand All @@ -136,7 +121,48 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
}
}

return { projects, externalNodes };
return {
projects: readProjectConfigurationsFromRootMap(projectRootMap),
externalNodes,
};
}

export function readProjectConfigurationsFromRootMap(
projectRootMap: Map<string, ProjectConfiguration>
) {
const projects: Record<string, ProjectConfiguration> = {};
// If there are projects that have the same name, that is an error.
// This object tracks name -> (all roots of projects with that name)
// to provide better error messaging.
const errors: Map<string, string[]> = new Map();

for (const [root, configuration] of projectRootMap.entries()) {
if (!configuration.name) {
throw new Error(`Project at ${root} has no name provided.`);
} else if (configuration.name in projects) {
let rootErrors = errors.get(configuration.name) ?? [
projects[configuration.name].root,
];
rootErrors.push(root);
errors.set(configuration.name, rootErrors);
} else {
projects[configuration.name] = configuration;
}
}

if (errors.size > 0) {
throw new Error(
[
`The following projects are defined in multiple locations:`,
...Array.from(errors.entries()).map(([project, roots]) =>
[`- ${project}: `, ...roots.map((r) => ` - ${r}`)].join('\n')
),
'',
"To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.",
].join('\n')
);
}
return projects;
}

export function mergeTargetConfigurations(
Expand Down
Loading