Skip to content

Commit ee135b2

Browse files
authored
feat(core): handle existing plugins failed with imported project (#28893)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> <img width="1025" alt="Screenshot 2024-12-21 at 9 51 18 PM" src="https://github.com/user-attachments/assets/32815566-c532-4186-bc94-4b017b0a84c2" /> ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent dec2166 commit ee135b2

File tree

9 files changed

+363
-30
lines changed

9 files changed

+363
-30
lines changed

docs/generated/devkit/AggregateCreateNodesError.md

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ It allows Nx to recieve partial results and continue processing for better UX.
2222
- [message](../../devkit/documents/AggregateCreateNodesError#message): string
2323
- [name](../../devkit/documents/AggregateCreateNodesError#name): string
2424
- [partialResults](../../devkit/documents/AggregateCreateNodesError#partialresults): CreateNodesResultV2
25+
- [pluginIndex](../../devkit/documents/AggregateCreateNodesError#pluginindex): number
2526
- [stack](../../devkit/documents/AggregateCreateNodesError#stack): string
2627
- [prepareStackTrace](../../devkit/documents/AggregateCreateNodesError#preparestacktrace): Function
2728
- [stackTraceLimit](../../devkit/documents/AggregateCreateNodesError#stacktracelimit): number
@@ -124,6 +125,12 @@ The partial results of the `createNodesV2` function. This should be the results
124125

125126
---
126127

128+
### pluginIndex
129+
130+
**pluginIndex**: `number`
131+
132+
---
133+
127134
### stack
128135

129136
`Optional` **stack**: `string`

packages/nx/src/command-line/import/import.ts

+41-12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import {
2929
configurePlugins,
3030
runPackageManagerInstallPlugins,
3131
} from '../init/configure-plugins';
32+
import {
33+
checkCompatibleWithPlugins,
34+
updatePluginsInNxJson,
35+
} from '../init/implementation/check-compatible-with-plugins';
3236

3337
const importRemoteName = '__tmp_nx_import__';
3438

@@ -286,21 +290,30 @@ export async function importHandler(options: ImportOptions) {
286290
packageManager,
287291
destinationGitClient
288292
);
289-
290-
if (installed && plugins.length > 0) {
291-
installed = await runPluginsInstall(plugins, pmc, destinationGitClient);
292-
if (installed) {
293-
const { succeededPlugins } = await configurePlugins(
294-
plugins,
295-
updatePackageScripts,
296-
pmc,
297-
workspaceRoot,
298-
verbose
299-
);
300-
if (succeededPlugins.length > 0) {
293+
if (installed) {
294+
// Check compatibility with existing plugins for the workspace included new imported projects
295+
if (nxJson.plugins?.length > 0) {
296+
const incompatiblePlugins = await checkCompatibleWithPlugins();
297+
if (Object.keys(incompatiblePlugins).length > 0) {
298+
updatePluginsInNxJson(workspaceRoot, incompatiblePlugins);
301299
await destinationGitClient.amendCommit();
302300
}
303301
}
302+
if (plugins.length > 0) {
303+
installed = await runPluginsInstall(plugins, pmc, destinationGitClient);
304+
if (installed) {
305+
const { succeededPlugins } = await configurePlugins(
306+
plugins,
307+
updatePackageScripts,
308+
pmc,
309+
workspaceRoot,
310+
verbose
311+
);
312+
if (succeededPlugins.length > 0) {
313+
await destinationGitClient.amendCommit();
314+
}
315+
}
316+
}
304317
}
305318

306319
console.log(await destinationGitClient.showStat());
@@ -313,6 +326,22 @@ export async function importHandler(options: ImportOptions) {
313326
`You may need to run "${pmc.install}" manually to resolve the issue. The error is logged above.`,
314327
],
315328
});
329+
if (plugins.length > 0) {
330+
output.error({
331+
title: `Failed to install plugins`,
332+
bodyLines: [
333+
'The following plugins were not installed:',
334+
...plugins.map((p) => `- ${chalk.bold(p)}`),
335+
],
336+
});
337+
output.error({
338+
title: `To install the plugins manually`,
339+
bodyLines: [
340+
'You may need to run commands to install the plugins:',
341+
...plugins.map((p) => `- ${chalk.bold(pmc.exec + ' nx add ' + p)}`),
342+
],
343+
});
344+
}
316345
}
317346

318347
if (source != destination) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
AggregateCreateNodesError,
3+
CreateMetadataError,
4+
MergeNodesError,
5+
ProjectGraphError,
6+
ProjectsWithNoNameError,
7+
} from '../../../project-graph/error-types';
8+
import { checkCompatibleWithPlugins } from './check-compatible-with-plugins';
9+
import { createProjectGraphAsync } from '../../../project-graph/project-graph';
10+
11+
jest.mock('../../../project-graph/project-graph', () => ({
12+
createProjectGraphAsync: jest.fn(),
13+
}));
14+
15+
describe('checkCompatibleWithPlugins', () => {
16+
beforeEach(() => {
17+
jest.clearAllMocks();
18+
});
19+
20+
it('should return empty object if no errors are thrown', async () => {
21+
(createProjectGraphAsync as any).mockReturnValueOnce(Promise.resolve({}));
22+
const result = await checkCompatibleWithPlugins();
23+
expect(result).toEqual({});
24+
});
25+
26+
it('should return empty object if error is not ProjectConfigurationsError', async () => {
27+
(createProjectGraphAsync as any).mockReturnValueOnce(
28+
Promise.reject(new Error('random error'))
29+
);
30+
const result = await checkCompatibleWithPlugins();
31+
expect(result).toEqual({});
32+
});
33+
34+
it('should return empty object if error is ProjectsWithNoNameError', async () => {
35+
(createProjectGraphAsync as any).mockReturnValueOnce(
36+
Promise.reject(
37+
new ProjectGraphError(
38+
[
39+
new ProjectsWithNoNameError([], {
40+
project1: { root: 'root1' },
41+
}),
42+
],
43+
undefined,
44+
undefined
45+
)
46+
)
47+
);
48+
const result = await checkCompatibleWithPlugins();
49+
expect(result).toEqual({});
50+
});
51+
52+
it('should return incompatible plugin with excluded files if error is AggregateCreateNodesError', async () => {
53+
const error = new AggregateCreateNodesError(
54+
[
55+
['file1', undefined],
56+
['file2', undefined],
57+
],
58+
[]
59+
);
60+
error.pluginIndex = 0;
61+
(createProjectGraphAsync as any).mockReturnValueOnce(
62+
Promise.reject(new ProjectGraphError([error], undefined, undefined))
63+
);
64+
const result = await checkCompatibleWithPlugins();
65+
expect(result).toEqual({
66+
0: [
67+
{ file: 'file1', error: undefined },
68+
{ file: 'file2', error: undefined },
69+
],
70+
});
71+
});
72+
73+
it('should return true if error is MergeNodesError', async () => {
74+
let error = new MergeNodesError({
75+
file: 'file2',
76+
pluginName: 'plugin2',
77+
error: new Error(),
78+
pluginIndex: 1,
79+
});
80+
(createProjectGraphAsync as any).mockReturnValueOnce(
81+
Promise.reject(new ProjectGraphError([error], undefined, undefined))
82+
);
83+
const result = await checkCompatibleWithPlugins();
84+
expect(result).toEqual({ 1: [{ error, file: 'file2' }] });
85+
});
86+
87+
it('should handle multiple errors', async () => {
88+
const mergeNodesError = new MergeNodesError({
89+
file: 'file2',
90+
pluginName: 'plugin2',
91+
error: new Error(),
92+
pluginIndex: 2,
93+
});
94+
const aggregateError0 = new AggregateCreateNodesError(
95+
[
96+
['file1', undefined],
97+
['file2', undefined],
98+
],
99+
[]
100+
);
101+
aggregateError0.pluginIndex = 0;
102+
const aggregateError2 = new AggregateCreateNodesError(
103+
[
104+
['file3', undefined],
105+
['file4', undefined],
106+
],
107+
[]
108+
);
109+
aggregateError2.pluginIndex = 2;
110+
(createProjectGraphAsync as any).mockReturnValueOnce(
111+
Promise.reject(
112+
new ProjectGraphError(
113+
[
114+
new ProjectsWithNoNameError([], {
115+
project1: { root: 'root1' },
116+
}),
117+
new CreateMetadataError(new Error(), 'file1'),
118+
new AggregateCreateNodesError([], []),
119+
aggregateError0,
120+
mergeNodesError,
121+
aggregateError2,
122+
],
123+
undefined,
124+
undefined
125+
)
126+
)
127+
);
128+
const result = await checkCompatibleWithPlugins();
129+
expect(result).toEqual({
130+
0: [{ file: 'file1' }, { file: 'file2' }],
131+
2: [
132+
{ file: 'file2', error: mergeNodesError },
133+
{ file: 'file3' },
134+
{ file: 'file4' },
135+
],
136+
});
137+
});
138+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { existsSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import { bold } from 'chalk';
4+
5+
import { NxJsonConfiguration } from '../../../config/nx-json';
6+
import {
7+
isAggregateCreateNodesError,
8+
isMergeNodesError,
9+
isProjectsWithNoNameError,
10+
ProjectGraphError,
11+
} from '../../../project-graph/error-types';
12+
import { workspaceRoot } from '../../../utils/workspace-root';
13+
import { readJsonFile, writeJsonFile } from '../../../utils/fileutils';
14+
import { output } from '../../../utils/output';
15+
import { createProjectGraphAsync } from '../../../project-graph/project-graph';
16+
17+
export interface IncompatibleFiles {
18+
[pluginIndex: number]: { file: string; error?: any }[];
19+
}
20+
21+
/**
22+
* This function checks if the imported project is compatible with the plugins.
23+
* @returns a map of plugin names to files that are incompatible with the plugins
24+
*/
25+
export async function checkCompatibleWithPlugins(): Promise<IncompatibleFiles> {
26+
let pluginToExcludeFiles: IncompatibleFiles = {};
27+
try {
28+
await createProjectGraphAsync();
29+
} catch (projectGraphError) {
30+
if (projectGraphError instanceof ProjectGraphError) {
31+
projectGraphError.getErrors()?.forEach((error) => {
32+
const { pluginIndex, excludeFiles } =
33+
findPluginAndFilesWithError(error) ?? {};
34+
if (pluginIndex !== undefined && excludeFiles?.length) {
35+
pluginToExcludeFiles[pluginIndex] ??= [];
36+
pluginToExcludeFiles[pluginIndex].push(...excludeFiles);
37+
} else if (!isProjectsWithNoNameError(error)) {
38+
// print error if it is not ProjectsWithNoNameError and unable to exclude files
39+
output.error({
40+
title: error.message,
41+
bodyLines: error.stack?.split('\n'),
42+
});
43+
}
44+
});
45+
} else {
46+
output.error({
47+
title:
48+
'Failed to process project graph. Run "nx reset" to fix this. Please report the issue if you keep seeing it.',
49+
bodyLines: projectGraphError.stack?.split('\n'),
50+
});
51+
}
52+
}
53+
return pluginToExcludeFiles;
54+
}
55+
56+
/**
57+
* This function finds the plugin name and files that caused the error.
58+
* @param error the error to find the plugin name and files for
59+
* @returns pluginName and excludeFiles if found, otherwise undefined
60+
*/
61+
function findPluginAndFilesWithError(
62+
error: any
63+
):
64+
| { pluginIndex: number; excludeFiles: { file: string; error?: any }[] }
65+
| undefined {
66+
let pluginIndex: number | undefined;
67+
let excludeFiles: { file: string; error?: any }[] = [];
68+
if (isAggregateCreateNodesError(error)) {
69+
pluginIndex = error.pluginIndex;
70+
excludeFiles =
71+
error.errors?.map((error) => {
72+
return {
73+
file: error?.[0],
74+
error: error?.[1],
75+
};
76+
}) ?? [];
77+
} else if (isMergeNodesError(error)) {
78+
pluginIndex = error.pluginIndex;
79+
excludeFiles = [
80+
{
81+
file: error.file,
82+
error: error,
83+
},
84+
];
85+
}
86+
excludeFiles = excludeFiles.filter(Boolean);
87+
return {
88+
pluginIndex,
89+
excludeFiles,
90+
};
91+
}
92+
93+
/**
94+
* This function updates the plugins in the nx.json file with the given plugin names and files to exclude.
95+
*/
96+
export function updatePluginsInNxJson(
97+
root: string = workspaceRoot,
98+
pluginToExcludeFiles: IncompatibleFiles
99+
): void {
100+
const nxJsonPath = join(root, 'nx.json');
101+
if (!existsSync(nxJsonPath)) {
102+
return;
103+
}
104+
let nxJson: NxJsonConfiguration;
105+
try {
106+
nxJson = readJsonFile<NxJsonConfiguration>(nxJsonPath);
107+
} catch {
108+
// If there is an error reading the nx.json file, no need to update it
109+
return;
110+
}
111+
if (!Object.keys(pluginToExcludeFiles)?.length || !nxJson?.plugins?.length) {
112+
return;
113+
}
114+
Object.entries(pluginToExcludeFiles).forEach(
115+
([pluginIndex, excludeFiles]) => {
116+
let plugin = nxJson.plugins[pluginIndex];
117+
if (!plugin || excludeFiles.length === 0) {
118+
return;
119+
}
120+
if (typeof plugin === 'string') {
121+
plugin = { plugin };
122+
}
123+
output.warn({
124+
title: `The following files were incompatible with ${plugin.plugin} and has been excluded for now:`,
125+
bodyLines: excludeFiles
126+
.map((file: { file: string; error?: any }) => {
127+
const output = [` - ${bold(file.file)}`];
128+
if (file.error?.message) {
129+
output.push(` ${file.error.message}`);
130+
}
131+
return output;
132+
})
133+
.flat(),
134+
});
135+
136+
const excludes = new Set(plugin.exclude ?? []);
137+
excludeFiles.forEach((file) => {
138+
excludes.add(file.file);
139+
});
140+
plugin.exclude = Array.from(excludes);
141+
nxJson.plugins[pluginIndex] = plugin;
142+
}
143+
);
144+
writeJsonFile(nxJsonPath, nxJson);
145+
}

0 commit comments

Comments
 (0)