feat(core): not exit when one plugin installation failed (#28684)

<!-- 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 -->
it tries to install all plugins at once. if failed, it will stop the
remaining plugins from being installed.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
it will continue if one plugin failed.

success message:
<img width="301" alt="Screenshot 2024-12-11 at 11 36 14 AM"
src="https://github.com/user-attachments/assets/2bc389f0-4fda-4959-afab-57594c9d600b">
failed message:
<img width="894" alt="Screenshot 2024-12-12 at 2 58 17 PM"
src="https://github.com/user-attachments/assets/7b51d5e9-f308-48c3-9b7d-bc4219802acb"
/>


## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Emily Xiong 2024-12-16 11:38:22 -08:00 committed by GitHub
parent 67d0e33874
commit 1d7465b02e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 401 additions and 189 deletions

View File

@ -772,7 +772,7 @@ describe('Linter', () => {
const mylib = uniq('mylib');
runCLI(
`generate @nx/node:app --name=${myapp} --linter=eslint --directory="." --e2eTestRunner=jest --no-interactive`
`generate @nx/node:app --name=${myapp} --linter=eslint --directory="." --unitTestRunner=jest --e2eTestRunner=jest --no-interactive`
);
runCLI('reset', { env: { CI: 'false' } });
verifySuccessfulStandaloneSetup(myapp);

View File

@ -15,8 +15,9 @@ import {
writeJson,
} from 'nx/src/devkit-exports';
import {
isProjectConfigurationsError,
isProjectsWithNoNameError,
LoadedNxPlugin,
ProjectConfigurationsError,
retrieveProjectConfigurations,
} from 'nx/src/devkit-internals';
@ -130,8 +131,12 @@ async function _addPluginInternal<PluginOptions>(
);
} catch (e) {
// Errors are okay for this because we're only running 1 plugin
if (e instanceof ProjectConfigurationsError) {
if (isProjectConfigurationsError(e)) {
projConfigs = e.partialProjectConfigurationsResult;
// ignore errors from projects with no name
if (!e.errors.every(isProjectsWithNoNameError)) {
throw e;
}
} else {
throw e;
}
@ -171,8 +176,12 @@ async function _addPluginInternal<PluginOptions>(
);
} catch (e) {
// Errors are okay for this because we're only running 1 plugin
if (e instanceof ProjectConfigurationsError) {
if (isProjectConfigurationsError(e)) {
projConfigs = e.partialProjectConfigurationsResult;
// ignore errors from projects with no name
if (!e.errors.every(isProjectsWithNoNameError)) {
throw e;
}
} else {
throw e;
}

View File

@ -98,7 +98,9 @@ export function assertNotUsingTsSolutionSetup(
],
});
process.exit(1);
throw new Error(
`The ${artifactString} doesn't yet support the existing TypeScript setup. See the error above.`
);
}
export function findRuntimeTsConfigName(

View File

@ -1,10 +1,9 @@
import { exec } from 'child_process';
import { existsSync } from 'fs';
import * as ora from 'ora';
import { isAngularPluginInstalled } from '../../adapter/angular-json';
import type { GeneratorsJsonEntry } from '../../config/misc-interfaces';
import * as yargsParser from 'yargs-parser';
import { readNxJson, type NxJsonConfiguration } from '../../config/nx-json';
import { runNxAsync, runNxSync } from '../../utils/child-process';
import { runNxAsync } from '../../utils/child-process';
import { writeJsonFile } from '../../utils/fileutils';
import { logger } from '../../utils/logger';
import { output } from '../../utils/output';
@ -14,12 +13,15 @@ import {
getPackageManagerVersion,
} from '../../utils/package-manager';
import { handleErrors } from '../../utils/handle-errors';
import { getPluginCapabilities } from '../../utils/plugins';
import { nxVersion } from '../../utils/versions';
import { workspaceRoot } from '../../utils/workspace-root';
import type { AddOptions } from './command-object';
import { normalizeVersionForNxJson } from '../init/implementation/dot-nx/add-nx-scripts';
import { gte } from 'semver';
import {
installPlugin,
getFailedToInstallPluginErrorMessages,
} from '../init/configure-plugins';
export function addHandler(options: AddOptions): Promise<number> {
return handleErrors(options.verbose, async () => {
@ -109,54 +111,47 @@ async function initializePlugin(
options: AddOptions,
nxJson: NxJsonConfiguration
): Promise<void> {
const capabilities = await getPluginCapabilities(workspaceRoot, pkgName, {});
const generators = capabilities?.generators;
if (!generators) {
output.log({
title: `No generators found in ${pkgName}. Skipping initialization.`,
});
return;
}
const parsedCommandArgs: { [key: string]: any } = yargsParser(
options.__overrides_unparsed__,
{
configuration: {
'parse-numbers': false,
'parse-positional-numbers': false,
'dot-notation': false,
'camel-case-expansion': false,
},
}
);
const initGenerator = findInitGenerator(generators);
if (!initGenerator) {
output.log({
title: `No "init" generator found in ${pkgName}. Skipping initialization.`,
});
return;
if (coreNxPluginVersions.has(pkgName)) {
parsedCommandArgs.keepExistingVersions = true;
if (
options.updatePackageScripts ||
(options.updatePackageScripts === undefined &&
nxJson.useInferencePlugins !== false &&
process.env.NX_ADD_PLUGINS !== 'false')
) {
parsedCommandArgs.updatePackageScripts = true;
}
}
const spinner = ora(`Initializing ${pkgName}...`);
spinner.start();
try {
const args = [];
if (coreNxPluginVersions.has(pkgName)) {
args.push(`--keepExistingVersions`);
if (
options.updatePackageScripts ||
(options.updatePackageScripts === undefined &&
nxJson.useInferencePlugins !== false &&
process.env.NX_ADD_PLUGINS !== 'false')
) {
args.push(`--updatePackageScripts`);
}
}
if (options.__overrides_unparsed__.length) {
args.push(...options.__overrides_unparsed__);
}
runNxSync(`g ${pkgName}:${initGenerator} ${args.join(' ')}`, {
stdio: [0, 1, 2],
});
await installPlugin(
pkgName,
workspaceRoot,
options.verbose,
parsedCommandArgs
);
} catch (e) {
spinner.fail();
output.addNewline();
logger.error(e);
output.error({
title: `Failed to initialize ${pkgName}. Please check the error above for more details.`,
title: `Failed to initialize ${pkgName}`,
bodyLines: getFailedToInstallPluginErrorMessages(e),
});
process.exit(1);
}
@ -164,25 +159,6 @@ async function initializePlugin(
spinner.succeed();
}
function findInitGenerator(
generators: Record<string, GeneratorsJsonEntry>
): string | undefined {
if (generators['init']) {
return 'init';
}
const angularPluginInstalled = isAngularPluginInstalled();
if (angularPluginInstalled && generators['ng-add']) {
return 'ng-add';
}
return Object.keys(generators).find(
(name) =>
generators[name].aliases?.includes('init') ||
(angularPluginInstalled && generators[name].aliases?.includes('ng-add'))
);
}
function parsePackageSpecifier(
packageSpecifier: string
): [pkgName: string, version: string] {

View File

@ -7,7 +7,7 @@ import { tmpdir } from 'tmp';
import { prompt } from 'enquirer';
import { output } from '../../utils/output';
import * as createSpinner from 'ora';
import { detectPlugins, installPlugins } from '../init/init-v2';
import { detectPlugins } from '../init/init-v2';
import { readNxJson } from '../../config/nx-json';
import { workspaceRoot } from '../../utils/workspace-root';
import {
@ -24,11 +24,11 @@ import { runInstall } from '../init/implementation/utils';
import { getBaseRef } from '../../utils/command-line-utils';
import { prepareSourceRepo } from './utils/prepare-source-repo';
import { mergeRemoteSource } from './utils/merge-remote-source';
import {
getPackagesInPackageManagerWorkspace,
needsInstall,
} from './utils/needs-install';
import { minimatch } from 'minimatch';
import {
configurePlugins,
runPackageManagerInstallPlugins,
} from '../init/configure-plugins';
const importRemoteName = '__tmp_nx_import__';
@ -60,7 +60,7 @@ export interface ImportOptions {
export async function importHandler(options: ImportOptions) {
process.env.NX_RUNNING_NX_IMPORT = 'true';
let { sourceRepository, ref, source, destination } = options;
let { sourceRepository, ref, source, destination, verbose } = options;
const destinationGitClient = new GitRepository(process.cwd());
if (await destinationGitClient.hasUncommittedChanges()) {
@ -219,11 +219,6 @@ export async function importHandler(options: ImportOptions) {
}
const packageManager = detectPackageManager(workspaceRoot);
const originalPackageWorkspaces = await getPackagesInPackageManagerWorkspace(
packageManager
);
const sourceIsNxWorkspace = existsSync(join(sourceGitClient.root, 'nx.json'));
const relativeDestination = relative(
@ -287,42 +282,30 @@ export async function importHandler(options: ImportOptions) {
destinationGitClient
);
// If install fails, we should continue since the errors could be resolved later.
let installFailed = false;
if (plugins.length > 0) {
try {
output.log({ title: 'Installing Plugins' });
installPlugins(workspaceRoot, plugins, pmc, updatePackageScripts);
let installed = await runInstallDestinationRepo(
packageManager,
destinationGitClient
);
await destinationGitClient.amendCommit();
} catch (e) {
installFailed = true;
output.error({
title: `Install failed: ${e.message || 'Unknown error'}`,
bodyLines: [e.stack],
});
}
} else if (await needsInstall(packageManager, originalPackageWorkspaces)) {
try {
output.log({
title: 'Installing dependencies for imported code',
});
runInstall(workspaceRoot, getPackageManagerCommand(packageManager));
await destinationGitClient.amendCommit();
} catch (e) {
installFailed = true;
output.error({
title: `Install failed: ${e.message || 'Unknown error'}`,
bodyLines: [e.stack],
});
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) {
await destinationGitClient.amendCommit();
}
}
}
console.log(await destinationGitClient.showStat());
if (installFailed) {
if (installed === false) {
const pmc = getPackageManagerCommand(packageManager);
output.warn({
title: `The import was successful, but the install failed`,
@ -397,6 +380,62 @@ async function createTemporaryRemote(
}
/**
* Run install for the imported code and plugins
* @returns true if the install failed
*/
async function runInstallDestinationRepo(
packageManager: PackageManager,
destinationGitClient: GitRepository
): Promise<boolean> {
let installed = true;
try {
output.log({
title: 'Installing dependencies for imported code',
});
runInstall(workspaceRoot, getPackageManagerCommand(packageManager));
await destinationGitClient.amendCommit();
} catch (e) {
installed = false;
output.error({
title: `Install failed: ${e.message || 'Unknown error'}`,
bodyLines: [e.stack],
});
}
return installed;
}
async function runPluginsInstall(
plugins: string[],
pmc: PackageManagerCommands,
destinationGitClient: GitRepository
) {
let installed = true;
output.log({ title: 'Installing Plugins' });
try {
runPackageManagerInstallPlugins(workspaceRoot, pmc, plugins);
await destinationGitClient.amendCommit();
} catch (e) {
installed = false;
output.error({
title: `Install failed: ${e.message || 'Unknown error'}`,
bodyLines: [
'The following plugins were not installed:',
...plugins.map((p) => `- ${chalk.bold(p)}`),
e.stack,
],
});
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)}`),
],
});
}
return installed;
}
/*
* If the user imports a project that isn't in the workspaces entry, we should add that path to the workspaces entry.
*/
async function handleMissingWorkspacesEntry(

View File

@ -1,44 +0,0 @@
import {
isWorkspacesEnabled,
PackageManager,
} from '../../../utils/package-manager';
import { workspaceRoot } from '../../../utils/workspace-root';
import { getGlobPatternsFromPackageManagerWorkspaces } from '../../../plugins/package-json';
import { globWithWorkspaceContext } from '../../../utils/workspace-context';
export async function getPackagesInPackageManagerWorkspace(
packageManager: PackageManager
) {
if (!isWorkspacesEnabled(packageManager, workspaceRoot)) {
return new Set<string>();
}
const patterns = getGlobPatternsFromPackageManagerWorkspaces(workspaceRoot);
return new Set(await globWithWorkspaceContext(workspaceRoot, patterns));
}
export async function needsInstall(
packageManager: PackageManager,
originalPackagesInPackageManagerWorkspaces: Set<string>
) {
if (!isWorkspacesEnabled(packageManager, workspaceRoot)) {
return false;
}
const updatedPackagesInPackageManagerWorkspaces =
await getPackagesInPackageManagerWorkspace(packageManager);
if (
updatedPackagesInPackageManagerWorkspaces.size !==
originalPackagesInPackageManagerWorkspaces.size
) {
return true;
}
for (const pkg of updatedPackagesInPackageManagerWorkspaces) {
if (!originalPackagesInPackageManagerWorkspaces.has(pkg)) {
return true;
}
}
return false;
}

View File

@ -1,6 +1,5 @@
import * as createSpinner from 'ora';
import { dirname, join, relative } from 'path';
import { mkdir, rm } from 'node:fs/promises';
import { join, relative } from 'path';
import { GitRepository } from '../../../utils/git-utils';
export async function prepareSourceRepo(

View File

@ -0,0 +1,253 @@
import * as createSpinner from 'ora';
import { bold } from 'chalk';
import {
getPackageManagerCommand,
PackageManagerCommands,
} from '../../utils/package-manager';
import { GitRepository } from '../../utils/git-utils';
import { output } from '../../utils/output';
import { flushChanges, FsTree } from '../../generators/tree';
import {
Generator as NxGenerator,
GeneratorCallback,
GeneratorsJsonEntry,
} from '../../config/misc-interfaces';
import { getGeneratorInformation } from '../generate/generator-utils';
import { workspaceRoot } from '../../utils/workspace-root';
import { addDepsToPackageJson, runInstall } from './implementation/utils';
import { getPluginCapabilities } from '../../utils/plugins';
import { isAngularPluginInstalled } from '../../adapter/angular-json';
import {
isAggregateCreateNodesError,
isProjectConfigurationsError,
isProjectsWithNoNameError,
} from '../../project-graph/error-types';
export function runPackageManagerInstallPlugins(
repoRoot: string,
pmc: PackageManagerCommands = getPackageManagerCommand(),
plugins: string[]
) {
if (plugins.length === 0) {
return;
}
addDepsToPackageJson(repoRoot, plugins);
runInstall(repoRoot, pmc);
}
/**
* Installs a plugin by running its init generator. It will change the file system tree passed in.
* @param plugin The name of the plugin to install
* @param repoRoot repo root
* @param verbose verbose
* @param options options passed to init generator
* @returns void
*/
export async function installPlugin(
plugin: string,
repoRoot: string = workspaceRoot,
verbose: boolean = false,
options: { [k: string]: any }
): Promise<void> {
const host = new FsTree(repoRoot, verbose, `install ${plugin}`);
const capabilities = await getPluginCapabilities(repoRoot, plugin, {});
const generators = capabilities?.generators;
if (!generators) {
throw new Error(`No generators found in ${plugin}.`);
}
const initGenerator = findInitGenerator(generators);
if (!initGenerator) {
output.log({
title: `No "init" generator found in ${plugin}. Skipping initialization.`,
});
return;
}
const { implementationFactory } = getGeneratorInformation(
plugin,
initGenerator,
repoRoot,
{}
);
const implementation: NxGenerator = implementationFactory();
const task: GeneratorCallback | void = await implementation(host, options);
flushChanges(repoRoot, host.listChanges());
if (task) {
await task();
}
}
/**
* Install plugins
* Get the implementation of the plugin's init generator and run it
* @returns a list of succeeded plugins and a map of failed plugins to errors
*/
export async function installPlugins(
plugins: string[],
updatePackageScripts: boolean,
repoRoot: string = workspaceRoot,
verbose: boolean = false
): Promise<{
succeededPlugins: string[];
failedPlugins: { [plugin: string]: Error };
}> {
if (plugins.length === 0) {
return {
succeededPlugins: [],
failedPlugins: {},
};
}
const spinner = createSpinner();
let succeededPlugins = [];
const failedPlugins: {
[pluginName: string]: Error;
} = {};
for (const plugin of plugins) {
try {
spinner.start('Installing plugin ' + plugin);
await installPlugin(plugin, repoRoot, verbose, {
keepExistingVersions: true,
updatePackageScripts,
addPlugin: true,
skipFormat: false,
skipPackageJson: false,
});
succeededPlugins.push(plugin);
spinner.succeed('Installed plugin ' + plugin);
} catch (e) {
failedPlugins[plugin] = e;
spinner.fail('Failed to install plugin ' + plugin);
}
}
return {
succeededPlugins,
failedPlugins,
};
}
/**
* Configures plugins, installs them, and outputs the results
* @returns a list of succeeded plugins and a map of failed plugins to errors
*/
export async function configurePlugins(
plugins: string[],
updatePackageScripts: boolean,
pmc: PackageManagerCommands,
repoRoot: string = workspaceRoot,
verbose: boolean = false
): Promise<{
succeededPlugins: string[];
failedPlugins: { [plugin: string]: Error };
}> {
if (plugins.length === 0) {
return {
succeededPlugins: [],
failedPlugins: {},
};
}
output.log({ title: '🔨 Configuring plugins' });
let { succeededPlugins, failedPlugins } = await installPlugins(
plugins,
updatePackageScripts,
repoRoot,
verbose
);
if (succeededPlugins.length > 0) {
output.success({
title: 'Installed Plugins',
bodyLines: succeededPlugins.map((p) => `- ${bold(p)}`),
});
}
if (Object.keys(failedPlugins).length > 0) {
output.error({
title: `Failed to install plugins`,
bodyLines: [
'The following plugins were not installed:',
...Object.keys(failedPlugins).map((p) => `- ${bold(p)}`),
],
});
Object.entries(failedPlugins).forEach(([plugin, error]) => {
output.error({
title: `Failed to install ${plugin}`,
bodyLines: getFailedToInstallPluginErrorMessages(error),
});
});
output.error({
title: `To install the plugins manually`,
bodyLines: [
'You may need to run commands to install the plugins:',
...Object.keys(failedPlugins).map(
(p) => `- ${bold(pmc.exec + ' nx add ' + p)}`
),
],
});
}
return { succeededPlugins, failedPlugins };
}
function findInitGenerator(
generators: Record<string, GeneratorsJsonEntry>
): string | undefined {
if (generators['init']) {
return 'init';
}
const angularPluginInstalled = isAngularPluginInstalled();
if (angularPluginInstalled && generators['ng-add']) {
return 'ng-add';
}
return Object.keys(generators).find(
(name) =>
generators[name].aliases?.includes('init') ||
(angularPluginInstalled && generators[name].aliases?.includes('ng-add'))
);
}
export function getFailedToInstallPluginErrorMessages(e: any): string[] {
const errorBodyLines = [];
if (isProjectConfigurationsError(e) && e.errors.length > 0) {
for (const error of e.errors) {
if (isAggregateCreateNodesError(error)) {
const innerErrors = error.errors;
for (const [file, e] of innerErrors) {
if (file) {
errorBodyLines.push(` - ${bold(file)}: ${e.message}`);
} else {
errorBodyLines.push(` - ${e.message}`);
}
if (e.stack) {
const innerStackTrace =
' ' + e.stack.split('\n')?.join('\n ');
errorBodyLines.push(innerStackTrace);
}
}
} else if (!isProjectsWithNoNameError(error)) {
// swallow ProjectsWithNameError
if (error.message) {
errorBodyLines.push(` - ${error.message}`);
}
if (error.stack) {
const innerStackTrace =
' ' + error.stack.split('\n')?.join('\n ');
errorBodyLines.push(innerStackTrace);
}
}
}
} else {
if (e.message) {
errorBodyLines.push(` - ${e.message}`);
}
if (e.stack) {
const innerStackTrace = ' ' + e.stack.split('\n')?.join('\n ');
errorBodyLines.push(innerStackTrace);
}
}
return errorBodyLines;
}

View File

@ -1,26 +1,21 @@
import { existsSync } from 'fs';
import { PackageJson } from '../../utils/package-json';
import { prerelease } from 'semver';
import { output } from '../../utils/output';
import {
getPackageManagerCommand,
PackageManagerCommands,
} from '../../utils/package-manager';
import { getPackageManagerCommand } from '../../utils/package-manager';
import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
import { runNxSync } from '../../utils/child-process';
import { readJsonFile } from '../../utils/fileutils';
import { nxVersion } from '../../utils/versions';
import {
addDepsToPackageJson,
createNxJsonFile,
initCloud,
isMonorepo,
printFinalMessage,
runInstall,
updateGitIgnore,
} from './implementation/utils';
import { prompt } from 'enquirer';
import { execSync } from 'child_process';
import { addNxToAngularCliRepo } from './implementation/angular';
import { globWithWorkspaceContextSync } from '../../utils/workspace-context';
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
@ -28,41 +23,17 @@ import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo';
import { addNxToMonorepo } from './implementation/add-nx-to-monorepo';
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
import { getPackageNameFromImportPath } from '../../utils/get-package-name-from-import-path';
import {
configurePlugins,
runPackageManagerInstallPlugins,
} from './configure-plugins';
export interface InitArgs {
interactive: boolean;
nxCloud?: boolean;
useDotNxInstallation?: boolean;
integrated?: boolean; // For Angular projects only
}
export function installPlugins(
repoRoot: string,
plugins: string[],
pmc: PackageManagerCommands,
updatePackageScripts: boolean
) {
if (plugins.length === 0) {
return;
}
addDepsToPackageJson(repoRoot, plugins);
runInstall(repoRoot, pmc);
output.log({ title: '🔨 Configuring plugins' });
for (const plugin of plugins) {
execSync(
`${pmc.exec} nx g ${plugin}:init --keepExistingVersions ${
updatePackageScripts ? '--updatePackageScripts' : ''
}`,
{
stdio: [0, 1, 2],
cwd: repoRoot,
windowsHide: false,
}
);
}
verbose?: boolean;
}
export async function initHandler(options: InitArgs): Promise<void> {
@ -146,7 +117,14 @@ export async function initHandler(options: InitArgs): Promise<void> {
output.log({ title: '📦 Installing Nx' });
installPlugins(repoRoot, plugins, pmc, updatePackageScripts);
runPackageManagerInstallPlugins(repoRoot, pmc, plugins);
await configurePlugins(
plugins,
updatePackageScripts,
pmc,
repoRoot,
options.verbose
);
if (useNxCloud) {
output.log({ title: '🛠️ Setting up Nx Cloud' });