Skip to content

Commit 94cc716

Browse files
authored
fix(core): name collisions during project inference should not error out if corrected by a project.json's name (#18665)
1 parent 47f8b7a commit 94cc716

File tree

5 files changed

+227
-60
lines changed

5 files changed

+227
-60
lines changed

docs/generated/devkit/CreateNodesContext.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
77
### Properties
88

99
- [nxJsonConfiguration](../../devkit/documents/CreateNodesContext#nxjsonconfiguration)
10-
- [projectsConfigurations](../../devkit/documents/CreateNodesContext#projectsconfigurations)
1110
- [workspaceRoot](../../devkit/documents/CreateNodesContext#workspaceroot)
1211

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

1918
---
2019

21-
### projectsConfigurations
22-
23-
`Readonly` **projectsConfigurations**: `Record`<`string`, [`ProjectConfiguration`](../../devkit/documents/ProjectConfiguration)\>
24-
25-
---
26-
2720
### workspaceRoot
2821

2922
`Readonly` **workspaceRoot**: `string`

packages/nx/src/generators/utils/project-configuration.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import {
1010
ProjectConfiguration,
1111
ProjectsConfigurations,
1212
} from '../../config/workspace-json-project-json';
13-
import { mergeProjectConfigurationIntoProjectsConfigurations } from '../../project-graph/utils/project-configuration-utils';
13+
import {
14+
mergeProjectConfigurationIntoRootMap,
15+
readProjectConfigurationsFromRootMap,
16+
} from '../../project-graph/utils/project-configuration-utils';
1417
import { retrieveProjectConfigurationPathsWithoutPluginInference } from '../../project-graph/utils/retrieve-workspace-files';
1518
import { output } from '../../utils/output';
1619
import { PackageJson } from '../../utils/package-json';
@@ -207,26 +210,20 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
207210
(r) => deletedFiles.indexOf(r) === -1
208211
);
209212

210-
const rootMap: Map<string, string> = new Map();
211-
return projectFiles.reduce((projects, projectFile) => {
213+
const rootMap: Map<string, ProjectConfiguration> = new Map();
214+
for (const projectFile of projectFiles) {
212215
if (basename(projectFile) === 'project.json') {
213216
const json = readJson(tree, projectFile);
214217
const config = buildProjectFromProjectJson(json, projectFile);
215-
mergeProjectConfigurationIntoProjectsConfigurations(
216-
projects,
217-
rootMap,
218-
config,
219-
projectFile
220-
);
218+
mergeProjectConfigurationIntoRootMap(rootMap, config, projectFile);
221219
} else {
222220
const packageJson = readJson<PackageJson>(tree, projectFile);
223221
const config = buildProjectConfigurationFromPackageJson(
224222
packageJson,
225223
projectFile,
226224
readNxJson(tree)
227225
);
228-
mergeProjectConfigurationIntoProjectsConfigurations(
229-
projects,
226+
mergeProjectConfigurationIntoRootMap(
230227
rootMap,
231228
// Inferred targets, tags, etc don't show up when running generators
232229
// This is to help avoid running into issues when trying to update the workspace
@@ -237,8 +234,9 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
237234
projectFile
238235
);
239236
}
240-
return projects;
241-
}, {});
237+
}
238+
239+
return readProjectConfigurationsFromRootMap(rootMap);
242240
}
243241

244242
/**

packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { TargetConfiguration } from '../../config/workspace-json-project-json';
21
import {
2+
ProjectConfiguration,
3+
TargetConfiguration,
4+
} from '../../config/workspace-json-project-json';
5+
import {
6+
mergeProjectConfigurationIntoRootMap,
37
mergeTargetConfigurations,
8+
readProjectConfigurationsFromRootMap,
49
readTargetDefaultsForTarget,
510
} from './project-configuration-utils';
611

@@ -352,3 +357,146 @@ describe('target defaults', () => {
352357
});
353358
});
354359
});
360+
361+
describe('mergeProjectConfigurationIntoRootMap', () => {
362+
it('should merge targets from different configurations', () => {
363+
const rootMap = new RootMapBuilder()
364+
.addProject({
365+
root: 'libs/lib-a',
366+
name: 'lib-a',
367+
targets: {
368+
echo: {
369+
command: 'echo lib-a',
370+
},
371+
},
372+
})
373+
.getRootMap();
374+
mergeProjectConfigurationIntoRootMap(
375+
rootMap,
376+
{
377+
root: 'libs/lib-a',
378+
name: 'lib-a',
379+
targets: {
380+
build: {
381+
command: 'tsc',
382+
},
383+
},
384+
},
385+
'inferred-project-config-file.ts'
386+
);
387+
expect(rootMap.get('libs/lib-a')).toMatchInlineSnapshot(`
388+
{
389+
"name": "lib-a",
390+
"root": "libs/lib-a",
391+
"targets": {
392+
"build": {
393+
"command": "tsc",
394+
},
395+
"echo": {
396+
"command": "echo lib-a",
397+
},
398+
},
399+
}
400+
`);
401+
});
402+
403+
it("shouldn't overwrite project name, unless merging project from project.json", () => {
404+
const rootMap = new RootMapBuilder()
405+
.addProject({
406+
name: 'bad-name',
407+
root: 'libs/lib-a',
408+
})
409+
.getRootMap();
410+
mergeProjectConfigurationIntoRootMap(
411+
rootMap,
412+
{
413+
name: 'other-bad-name',
414+
root: 'libs/lib-a',
415+
},
416+
'libs/lib-a/package.json'
417+
);
418+
expect(rootMap.get('libs/lib-a').name).toEqual('bad-name');
419+
mergeProjectConfigurationIntoRootMap(
420+
rootMap,
421+
{
422+
name: 'lib-a',
423+
root: 'libs/lib-a',
424+
},
425+
'libs/lib-a/project.json'
426+
);
427+
expect(rootMap.get('libs/lib-a').name).toEqual('lib-a');
428+
});
429+
});
430+
431+
describe('readProjectsConfigurationsFromRootMap', () => {
432+
it('should error if multiple roots point to the same project', () => {
433+
const rootMap = new RootMapBuilder()
434+
.addProject({
435+
name: 'lib',
436+
root: 'apps/lib-a',
437+
})
438+
.addProject({
439+
name: 'lib',
440+
root: 'apps/lib-b',
441+
})
442+
.getRootMap();
443+
444+
expect(() => {
445+
readProjectConfigurationsFromRootMap(rootMap);
446+
}).toThrowErrorMatchingInlineSnapshot(`
447+
"The following projects are defined in multiple locations:
448+
- lib:
449+
- apps/lib-a
450+
- apps/lib-b
451+
452+
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."
453+
`);
454+
});
455+
456+
it('should read root map into standard projects configurations form', () => {
457+
const rootMap = new RootMapBuilder()
458+
.addProject({
459+
name: 'lib-a',
460+
root: 'libs/a',
461+
})
462+
.addProject({
463+
name: 'lib-b',
464+
root: 'libs/b',
465+
})
466+
.addProject({
467+
name: 'lib-shared-b',
468+
root: 'libs/shared/b',
469+
})
470+
.getRootMap();
471+
expect(readProjectConfigurationsFromRootMap(rootMap))
472+
.toMatchInlineSnapshot(`
473+
{
474+
"lib-a": {
475+
"name": "lib-a",
476+
"root": "libs/a",
477+
},
478+
"lib-b": {
479+
"name": "lib-b",
480+
"root": "libs/b",
481+
},
482+
"lib-shared-b": {
483+
"name": "lib-shared-b",
484+
"root": "libs/shared/b",
485+
},
486+
}
487+
`);
488+
});
489+
});
490+
491+
class RootMapBuilder {
492+
private rootMap: Map<string, ProjectConfiguration> = new Map();
493+
494+
addProject(p: ProjectConfiguration) {
495+
this.rootMap.set(p.root, p);
496+
return this;
497+
}
498+
499+
getRootMap() {
500+
return this.rootMap;
501+
}
502+
}

packages/nx/src/project-graph/utils/project-configuration-utils.ts

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,53 +8,42 @@ import {
88
ProjectConfiguration,
99
TargetConfiguration,
1010
} from '../../config/workspace-json-project-json';
11-
import { readJsonFile } from '../../utils/fileutils';
1211
import { NX_PREFIX } from '../../utils/logger';
1312
import { NxPluginV2 } from '../../utils/nx-plugin';
1413
import { workspaceRoot } from '../../utils/workspace-root';
1514

1615
import minimatch = require('minimatch');
17-
export function mergeProjectConfigurationIntoProjectsConfigurations(
18-
// projectName -> ProjectConfiguration
19-
existingProjects: Record<string, ProjectConfiguration>,
20-
// projectRoot -> projectName
21-
existingProjectRootMap: Map<string, string>,
16+
17+
export function mergeProjectConfigurationIntoRootMap(
18+
projectRootMap: Map<string, ProjectConfiguration>,
2219
project: ProjectConfiguration,
2320
// project.json is a special case, so we need to detect it.
2421
file: string
2522
): void {
26-
let matchingProjectName = existingProjectRootMap.get(project.root);
23+
const matchingProject = projectRootMap.get(project.root);
2724

28-
if (!matchingProjectName) {
29-
existingProjects[project.name] = project;
30-
existingProjectRootMap.set(project.root, project.name);
25+
if (!matchingProject) {
26+
projectRootMap.set(project.root, project);
3127
return;
32-
// There are some special cases for handling project.json - mainly
33-
// that it should override any name the project already has.
3428
} else if (
3529
project.name &&
36-
project.name !== matchingProjectName &&
30+
project.name !== matchingProject.name &&
3731
basename(file) === 'project.json'
3832
) {
39-
// Copy config to new name
40-
existingProjects[project.name] = existingProjects[matchingProjectName];
41-
// Update name in project config
42-
existingProjects[project.name].name = project.name;
43-
// Update root map to point to new name
44-
existingProjectRootMap[project.root] = project.name;
45-
// Remove entry for old name
46-
delete existingProjects[matchingProjectName];
47-
// Update name that config should be merged to
48-
matchingProjectName = project.name;
33+
// `name` inside project.json overrides any names from
34+
// inference plugins
35+
matchingProject.name = project.name;
4936
}
5037

51-
const matchingProject = existingProjects[matchingProjectName];
52-
53-
// This handles top level properties that are overwritten. `srcRoot`, `projectType`, or fields that Nx doesn't know about.
38+
// This handles top level properties that are overwritten.
39+
// e.g. `srcRoot`, `projectType`, or other fields that shouldn't be extended
40+
// Note: `name` is set specifically here to keep it from changing. The name is
41+
// always determined by the first inference plugin to ID a project, unless it has
42+
// a project.json in which case it was already updated above.
5443
const updatedProjectConfiguration = {
5544
...matchingProject,
5645
...project,
57-
name: matchingProjectName,
46+
name: matchingProject.name,
5847
};
5948

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

86-
if (updatedProjectConfiguration.name !== matchingProject.name) {
87-
delete existingProjects[matchingProject.name];
88-
}
89-
existingProjects[updatedProjectConfiguration.name] =
90-
updatedProjectConfiguration;
75+
projectRootMap.set(
76+
updatedProjectConfiguration.root,
77+
updatedProjectConfiguration
78+
);
9179
}
9280

9381
export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
@@ -99,8 +87,7 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
9987
projects: Record<string, ProjectConfiguration>;
10088
externalNodes: Record<string, ProjectGraphExternalNode>;
10189
} {
102-
const projectRootMap: Map<string, string> = new Map();
103-
const projects: Record<string, ProjectConfiguration> = {};
90+
const projectRootMap: Map<string, ProjectConfiguration> = new Map();
10491
const externalNodes: Record<string, ProjectGraphExternalNode> = {};
10592

10693
// We push the nx core node builder onto the end, s.t. it overwrites any user specified behavior
@@ -119,13 +106,11 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
119106
if (minimatch(file, pattern)) {
120107
const { projects: projectNodes, externalNodes: pluginExternalNodes } =
121108
configurationConstructor(file, {
122-
projectsConfigurations: projects,
123109
nxJsonConfiguration: nxJson,
124110
workspaceRoot: root,
125111
});
126112
for (const node in projectNodes) {
127-
mergeProjectConfigurationIntoProjectsConfigurations(
128-
projects,
113+
mergeProjectConfigurationIntoRootMap(
129114
projectRootMap,
130115
projectNodes[node],
131116
file
@@ -136,7 +121,48 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
136121
}
137122
}
138123

139-
return { projects, externalNodes };
124+
return {
125+
projects: readProjectConfigurationsFromRootMap(projectRootMap),
126+
externalNodes,
127+
};
128+
}
129+
130+
export function readProjectConfigurationsFromRootMap(
131+
projectRootMap: Map<string, ProjectConfiguration>
132+
) {
133+
const projects: Record<string, ProjectConfiguration> = {};
134+
// If there are projects that have the same name, that is an error.
135+
// This object tracks name -> (all roots of projects with that name)
136+
// to provide better error messaging.
137+
const errors: Map<string, string[]> = new Map();
138+
139+
for (const [root, configuration] of projectRootMap.entries()) {
140+
if (!configuration.name) {
141+
throw new Error(`Project at ${root} has no name provided.`);
142+
} else if (configuration.name in projects) {
143+
let rootErrors = errors.get(configuration.name) ?? [
144+
projects[configuration.name].root,
145+
];
146+
rootErrors.push(root);
147+
errors.set(configuration.name, rootErrors);
148+
} else {
149+
projects[configuration.name] = configuration;
150+
}
151+
}
152+
153+
if (errors.size > 0) {
154+
throw new Error(
155+
[
156+
`The following projects are defined in multiple locations:`,
157+
...Array.from(errors.entries()).map(([project, roots]) =>
158+
[`- ${project}: `, ...roots.map((r) => ` - ${r}`)].join('\n')
159+
),
160+
'',
161+
"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.",
162+
].join('\n')
163+
);
164+
}
165+
return projects;
140166
}
141167

142168
export function mergeTargetConfigurations(

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy