Skip to content

Commit

Permalink
feat(core): Handle when an existing plugin begins to fail with the ne…
Browse files Browse the repository at this point in the history
…w imported project
  • Loading branch information
xiongemi committed Jan 10, 2025
1 parent dd9b09f commit de5f133
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 32 deletions.
10 changes: 9 additions & 1 deletion 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 All @@ -34,7 +35,7 @@ It allows Nx to recieve partial results and continue processing for better UX.

### constructor

**new AggregateCreateNodesError**(`errors`, `partialResults`): [`AggregateCreateNodesError`](../../devkit/documents/AggregateCreateNodesError)
**new AggregateCreateNodesError**(`errors`, `partialResults`, `pluginIndex?`): [`AggregateCreateNodesError`](../../devkit/documents/AggregateCreateNodesError)

Throwing this error from a `createNodesV2` function will allow Nx to continue processing and recieve partial results from your plugin.

Expand All @@ -44,6 +45,7 @@ Throwing this error from a `createNodesV2` function will allow Nx to continue pr
| :--------------- | :------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `errors` | [file: string, error: Error][] | An array of tuples that represent errors encountered when processing a given file. An example entry might look like ['path/to/project.json', [Error: 'Invalid JSON. Unexpected token 'a' in JSON at position 0]] |
| `partialResults` | [`CreateNodesResultV2`](../../devkit/documents/CreateNodesResultV2) | The partial results of the `createNodesV2` function. This should be the results for each file that didn't encounter an issue. |
| `pluginIndex?` | `number` | - |

#### Returns

Expand Down Expand Up @@ -124,6 +126,12 @@ The partial results of the `createNodesV2` function. This should be the results

---

### pluginIndex

`Optional` **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,145 @@
import {
AggregateCreateNodesError,
CreateMetadataError,
MergeNodesError,
ProjectGraphError,
ProjectsWithNoNameError,
} from '../../../project-graph/error-types';
import { checkCompatibleWithPlugins } from './check-compatible-with-plugins';
import { retrieveProjectConfigurations } from '../../../project-graph/utils/retrieve-workspace-files';

jest.mock('../../../project-graph/utils/retrieve-workspace-files', () => ({
retrieveProjectConfigurations: jest.fn(),
}));

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

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

it('should return empty object if error is not ProjectConfigurationsError', async () => {
(retrieveProjectConfigurations 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 () => {
(retrieveProjectConfigurations 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 () => {
(retrieveProjectConfigurations as any).mockReturnValueOnce(
Promise.reject(
new ProjectGraphError(
[
new AggregateCreateNodesError(
[
['file1', undefined],
['file2', undefined],
],
[],
0
),
],
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,
});
(retrieveProjectConfigurations 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 () => {
let mergeNodesError = new MergeNodesError({
file: 'file2',
pluginName: 'plugin2',
error: new Error(),
pluginIndex: 2,
});
(retrieveProjectConfigurations as any).mockReturnValueOnce(
Promise.reject(
new ProjectGraphError(
[
new ProjectsWithNoNameError([], {
project1: { root: 'root1' },
}),
new CreateMetadataError(new Error(), 'file1'),
new AggregateCreateNodesError([], [], 0),
new AggregateCreateNodesError(
[
['file1', undefined],
['file2', undefined],
],
[],
0
),
mergeNodesError,
new AggregateCreateNodesError(
[
['file3', undefined],
['file4', undefined],
],
[],
2
),
],
undefined,
undefined
)
)
);
const result = await checkCompatibleWithPlugins();
expect(result).toEqual({
0: [{ file: 'file1' }, { file: 'file2' }],
2: [
{ file: 'file2', error: mergeNodesError },
{ file: 'file3' },
{ file: 'file4' },
],
});
});
});
Loading

0 comments on commit de5f133

Please # to comment.