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 Nov 12, 2024
1 parent c21b039 commit 8c0a118
Show file tree
Hide file tree
Showing 8 changed files with 354 additions and 3 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
- [pluginName](../../devkit/documents/AggregateCreateNodesError#pluginname): string
- [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`, `pluginName?`): [`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. |
| `pluginName?` | `string` | - |

#### Returns

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

---

### pluginName

`Optional` **pluginName**: `string`

---

### stack

`Optional` **stack**: `string`
Expand Down
1 change: 1 addition & 0 deletions packages/gradle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"generators": "./generators.json",
"exports": {
".": "./index.js",
"./plugin": "./plugin.js",
"./package.json": "./package.json",
"./migrations.json": "./migrations.json",
"./generators.json": "./generators.json"
Expand Down
24 changes: 24 additions & 0 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 {
needsInstall,
} from './utils/needs-install';
import { readPackageJson } from '../../project-graph/file-utils';
import {
checkCompatibleWithPlugins,
updatePluginsInNxJson,
} from './utils/check-compatible-with-plugins';

const importRemoteName = '__tmp_nx_import__';

Expand Down Expand Up @@ -282,6 +286,17 @@ export async function importHandler(options: ImportOptions) {

// If install fails, we should continue since the errors could be resolved later.
let installFailed = false;
if (nxJson.plugins?.length > 0) {
// Check compatibility with existing plugins for the workspace included new imported projects
const imcompatiblePlugins = await checkCompatibleWithPlugins(
nxJson.plugins,
workspaceRoot
);
if (Object.keys(imcompatiblePlugins).length > 0) {
updatePluginsInNxJson(workspaceRoot, imcompatiblePlugins);
await destinationGitClient.amendCommit();
}
}
if (plugins.length > 0) {
try {
output.log({ title: 'Installing Plugins' });
Expand All @@ -295,6 +310,15 @@ export async function importHandler(options: ImportOptions) {
bodyLines: [e.stack],
});
}
// Check compatibility with new plugins for the workspace included new imported projects
const imcompatiblePlugins = await checkCompatibleWithPlugins(
plugins.map((plugin) => plugin + '/plugin'), // plugins contains package name, but we need plugin name
workspaceRoot
);
if (Object.keys(imcompatiblePlugins).length > 0) {
updatePluginsInNxJson(workspaceRoot, imcompatiblePlugins);
await destinationGitClient.amendCommit();
}
} else if (await needsInstall(packageManager, originalPackageWorkspaces)) {
try {
output.log({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
AggregateCreateNodesError,
MergeNodesError,
ProjectConfigurationsError,
ProjectsWithNoNameError,
} from '../../../project-graph/error-types';
import { checkCompatibleWithPlugins } from './check-compatible-with-plugins';
import { retrieveProjectConfigurations } from '../../../project-graph/utils/retrieve-workspace-files';
import { tmpdir } from 'os';

jest.mock('../../../project-graph/plugins/internal-api', () => ({
loadNxPlugins: jest.fn().mockReturnValue([[], []]),
}));
jest.mock('../../../project-graph/utils/retrieve-workspace-files', () => ({
retrieveProjectConfigurations: jest.fn(),
}));

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

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

it('should return emptry object if error is not ProjectConfigurationsError', async () => {
(retrieveProjectConfigurations as any).mockReturnValueOnce(
Promise.reject(new Error('random error'))
);
const result = await checkCompatibleWithPlugins(
['plugin1', 'plugin2'],
'projectRootPathToCheck',
tmpdir()
);
expect(result).toEqual({});
});

it('should return emptry object if error is ProjectsWithNoNameError', async () => {
(retrieveProjectConfigurations as any).mockReturnValueOnce(
Promise.reject(
new ProjectConfigurationsError(
[
new ProjectsWithNoNameError([], {
project1: { root: 'root1' },
}),
],
undefined
)
)
);
const result = await checkCompatibleWithPlugins(
['plugin1', 'plugin2'],
'projectRootPathToCheck',
tmpdir()
);
expect(result).toEqual({});
});

it('should return imcompatible plugin with excluded files if error is AggregateCreateNodesError', async () => {
(retrieveProjectConfigurations as any).mockReturnValueOnce(
Promise.reject(
new ProjectConfigurationsError(
[
new AggregateCreateNodesError(
[
['file1', undefined],
['file2', undefined],
],
[],
'plugin1'
),
],
undefined
)
)
);
const result = await checkCompatibleWithPlugins(
['plugin1', 'plugin2'],
'projectRootPathToCheck',
tmpdir()
);
expect(result).toEqual({ plugin1: ['file1', 'file2'] });
});

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

it('should handle multiple errors', async () => {
(retrieveProjectConfigurations as any).mockReturnValueOnce(
Promise.reject(
new ProjectConfigurationsError(
[
new ProjectsWithNoNameError([], {
project1: { root: 'root1' },
}),
new AggregateCreateNodesError([], [], 'randomPlugin'),
new AggregateCreateNodesError(
[
['file1', undefined],
['file2', undefined],
],
[],
'plugin1'
),
new MergeNodesError({
file: 'file2',
pluginName: 'plugin2',
error: new Error(),
}),
],
undefined
)
)
);
const result = await checkCompatibleWithPlugins(
['plugin1', 'plugin2'],
'projectRootPathToCheck',
tmpdir()
);
expect(result).toEqual({ plugin1: ['file1', 'file2'], plugin2: ['file2'] });
});
});
Loading

0 comments on commit 8c0a118

Please # to comment.