Skip to content

Commit

Permalink
feat(core): handle existing plugins failed with imported project (#28893
Browse files Browse the repository at this point in the history
)

<!-- 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 #
  • Loading branch information
xiongemi authored Jan 17, 2025
1 parent dec2166 commit ee135b2
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 30 deletions.
7 changes: 7 additions & 0 deletions docs/generated/devkit/AggregateCreateNodesError.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ It allows Nx to recieve partial results and continue processing for better UX.
- [message](../../devkit/documents/AggregateCreateNodesError#message): string
- [name](../../devkit/documents/AggregateCreateNodesError#name): string
- [partialResults](../../devkit/documents/AggregateCreateNodesError#partialresults): CreateNodesResultV2
- [pluginIndex](../../devkit/documents/AggregateCreateNodesError#pluginindex): number
- [stack](../../devkit/documents/AggregateCreateNodesError#stack): string
- [prepareStackTrace](../../devkit/documents/AggregateCreateNodesError#preparestacktrace): Function
- [stackTraceLimit](../../devkit/documents/AggregateCreateNodesError#stacktracelimit): number
Expand Down Expand Up @@ -124,6 +125,12 @@ The partial results of the `createNodesV2` function. This should be the results

---

### pluginIndex

**pluginIndex**: `number`

---

### stack

`Optional` **stack**: `string`
Expand Down
53 changes: 41 additions & 12 deletions packages/nx/src/command-line/import/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
configurePlugins,
runPackageManagerInstallPlugins,
} from '../init/configure-plugins';
import {
checkCompatibleWithPlugins,
updatePluginsInNxJson,
} from '../init/implementation/check-compatible-with-plugins';

const importRemoteName = '__tmp_nx_import__';

Expand Down Expand Up @@ -286,21 +290,30 @@ export async function importHandler(options: ImportOptions) {
packageManager,
destinationGitClient
);

if (installed && plugins.length > 0) {
installed = await runPluginsInstall(plugins, pmc, destinationGitClient);
if (installed) {
const { succeededPlugins } = await configurePlugins(
plugins,
updatePackageScripts,
pmc,
workspaceRoot,
verbose
);
if (succeededPlugins.length > 0) {
if (installed) {
// Check compatibility with existing plugins for the workspace included new imported projects
if (nxJson.plugins?.length > 0) {
const incompatiblePlugins = await checkCompatibleWithPlugins();
if (Object.keys(incompatiblePlugins).length > 0) {
updatePluginsInNxJson(workspaceRoot, incompatiblePlugins);
await destinationGitClient.amendCommit();
}
}
if (plugins.length > 0) {
installed = await runPluginsInstall(plugins, pmc, destinationGitClient);
if (installed) {
const { succeededPlugins } = await configurePlugins(
plugins,
updatePackageScripts,
pmc,
workspaceRoot,
verbose
);
if (succeededPlugins.length > 0) {
await destinationGitClient.amendCommit();
}
}
}
}

console.log(await destinationGitClient.showStat());
Expand All @@ -313,6 +326,22 @@ export async function importHandler(options: ImportOptions) {
`You may need to run "${pmc.install}" manually to resolve the issue. The error is logged above.`,
],
});
if (plugins.length > 0) {
output.error({
title: `Failed to install plugins`,
bodyLines: [
'The following plugins were not installed:',
...plugins.map((p) => `- ${chalk.bold(p)}`),
],
});
output.error({
title: `To install the plugins manually`,
bodyLines: [
'You may need to run commands to install the plugins:',
...plugins.map((p) => `- ${chalk.bold(pmc.exec + ' nx add ' + p)}`),
],
});
}
}

if (source != destination) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
AggregateCreateNodesError,
CreateMetadataError,
MergeNodesError,
ProjectGraphError,
ProjectsWithNoNameError,
} from '../../../project-graph/error-types';
import { checkCompatibleWithPlugins } from './check-compatible-with-plugins';
import { createProjectGraphAsync } from '../../../project-graph/project-graph';

jest.mock('../../../project-graph/project-graph', () => ({
createProjectGraphAsync: jest.fn(),
}));

describe('checkCompatibleWithPlugins', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should return empty object if no errors are thrown', async () => {
(createProjectGraphAsync as any).mockReturnValueOnce(Promise.resolve({}));
const result = await checkCompatibleWithPlugins();
expect(result).toEqual({});
});

it('should return empty object if error is not ProjectConfigurationsError', async () => {
(createProjectGraphAsync as any).mockReturnValueOnce(
Promise.reject(new Error('random error'))
);
const result = await checkCompatibleWithPlugins();
expect(result).toEqual({});
});

it('should return empty object if error is ProjectsWithNoNameError', async () => {
(createProjectGraphAsync as any).mockReturnValueOnce(
Promise.reject(
new ProjectGraphError(
[
new ProjectsWithNoNameError([], {
project1: { root: 'root1' },
}),
],
undefined,
undefined
)
)
);
const result = await checkCompatibleWithPlugins();
expect(result).toEqual({});
});

it('should return incompatible plugin with excluded files if error is AggregateCreateNodesError', async () => {
const error = new AggregateCreateNodesError(
[
['file1', undefined],
['file2', undefined],
],
[]
);
error.pluginIndex = 0;
(createProjectGraphAsync as any).mockReturnValueOnce(
Promise.reject(new ProjectGraphError([error], undefined, undefined))
);
const result = await checkCompatibleWithPlugins();
expect(result).toEqual({
0: [
{ file: 'file1', error: undefined },
{ file: 'file2', error: undefined },
],
});
});

it('should return true if error is MergeNodesError', async () => {
let error = new MergeNodesError({
file: 'file2',
pluginName: 'plugin2',
error: new Error(),
pluginIndex: 1,
});
(createProjectGraphAsync as any).mockReturnValueOnce(
Promise.reject(new ProjectGraphError([error], undefined, undefined))
);
const result = await checkCompatibleWithPlugins();
expect(result).toEqual({ 1: [{ error, file: 'file2' }] });
});

it('should handle multiple errors', async () => {
const mergeNodesError = new MergeNodesError({
file: 'file2',
pluginName: 'plugin2',
error: new Error(),
pluginIndex: 2,
});
const aggregateError0 = new AggregateCreateNodesError(
[
['file1', undefined],
['file2', undefined],
],
[]
);
aggregateError0.pluginIndex = 0;
const aggregateError2 = new AggregateCreateNodesError(
[
['file3', undefined],
['file4', undefined],
],
[]
);
aggregateError2.pluginIndex = 2;
(createProjectGraphAsync as any).mockReturnValueOnce(
Promise.reject(
new ProjectGraphError(
[
new ProjectsWithNoNameError([], {
project1: { root: 'root1' },
}),
new CreateMetadataError(new Error(), 'file1'),
new AggregateCreateNodesError([], []),
aggregateError0,
mergeNodesError,
aggregateError2,
],
undefined,
undefined
)
)
);
const result = await checkCompatibleWithPlugins();
expect(result).toEqual({
0: [{ file: 'file1' }, { file: 'file2' }],
2: [
{ file: 'file2', error: mergeNodesError },
{ file: 'file3' },
{ file: 'file4' },
],
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { bold } from 'chalk';

import { NxJsonConfiguration } from '../../../config/nx-json';
import {
isAggregateCreateNodesError,
isMergeNodesError,
isProjectsWithNoNameError,
ProjectGraphError,
} from '../../../project-graph/error-types';
import { workspaceRoot } from '../../../utils/workspace-root';
import { readJsonFile, writeJsonFile } from '../../../utils/fileutils';
import { output } from '../../../utils/output';
import { createProjectGraphAsync } from '../../../project-graph/project-graph';

export interface IncompatibleFiles {
[pluginIndex: number]: { file: string; error?: any }[];
}

/**
* This function checks if the imported project is compatible with the plugins.
* @returns a map of plugin names to files that are incompatible with the plugins
*/
export async function checkCompatibleWithPlugins(): Promise<IncompatibleFiles> {
let pluginToExcludeFiles: IncompatibleFiles = {};
try {
await createProjectGraphAsync();
} catch (projectGraphError) {
if (projectGraphError instanceof ProjectGraphError) {
projectGraphError.getErrors()?.forEach((error) => {
const { pluginIndex, excludeFiles } =
findPluginAndFilesWithError(error) ?? {};
if (pluginIndex !== undefined && excludeFiles?.length) {
pluginToExcludeFiles[pluginIndex] ??= [];
pluginToExcludeFiles[pluginIndex].push(...excludeFiles);
} else if (!isProjectsWithNoNameError(error)) {
// print error if it is not ProjectsWithNoNameError and unable to exclude files
output.error({
title: error.message,
bodyLines: error.stack?.split('\n'),
});
}
});
} else {
output.error({
title:
'Failed to process project graph. Run "nx reset" to fix this. Please report the issue if you keep seeing it.',
bodyLines: projectGraphError.stack?.split('\n'),
});
}
}
return pluginToExcludeFiles;
}

/**
* This function finds the plugin name and files that caused the error.
* @param error the error to find the plugin name and files for
* @returns pluginName and excludeFiles if found, otherwise undefined
*/
function findPluginAndFilesWithError(
error: any
):
| { pluginIndex: number; excludeFiles: { file: string; error?: any }[] }
| undefined {
let pluginIndex: number | undefined;
let excludeFiles: { file: string; error?: any }[] = [];
if (isAggregateCreateNodesError(error)) {
pluginIndex = error.pluginIndex;
excludeFiles =
error.errors?.map((error) => {
return {
file: error?.[0],
error: error?.[1],
};
}) ?? [];
} else if (isMergeNodesError(error)) {
pluginIndex = error.pluginIndex;
excludeFiles = [
{
file: error.file,
error: error,
},
];
}
excludeFiles = excludeFiles.filter(Boolean);
return {
pluginIndex,
excludeFiles,
};
}

/**
* This function updates the plugins in the nx.json file with the given plugin names and files to exclude.
*/
export function updatePluginsInNxJson(
root: string = workspaceRoot,
pluginToExcludeFiles: IncompatibleFiles
): void {
const nxJsonPath = join(root, 'nx.json');
if (!existsSync(nxJsonPath)) {
return;
}
let nxJson: NxJsonConfiguration;
try {
nxJson = readJsonFile<NxJsonConfiguration>(nxJsonPath);
} catch {
// If there is an error reading the nx.json file, no need to update it
return;
}
if (!Object.keys(pluginToExcludeFiles)?.length || !nxJson?.plugins?.length) {
return;
}
Object.entries(pluginToExcludeFiles).forEach(
([pluginIndex, excludeFiles]) => {
let plugin = nxJson.plugins[pluginIndex];
if (!plugin || excludeFiles.length === 0) {
return;
}
if (typeof plugin === 'string') {
plugin = { plugin };
}
output.warn({
title: `The following files were incompatible with ${plugin.plugin} and has been excluded for now:`,
bodyLines: excludeFiles
.map((file: { file: string; error?: any }) => {
const output = [` - ${bold(file.file)}`];
if (file.error?.message) {
output.push(` ${file.error.message}`);
}
return output;
})
.flat(),
});

const excludes = new Set(plugin.exclude ?? []);
excludeFiles.forEach((file) => {
excludes.add(file.file);
});
plugin.exclude = Array.from(excludes);
nxJson.plugins[pluginIndex] = plugin;
}
);
writeJsonFile(nxJsonPath, nxJson);
}
Loading

0 comments on commit ee135b2

Please sign in to comment.