Skip to content

Commit d16c590

Browse files
committed
fix(@angular/cli): revert package.json changes if installation tasks fail during update
When running the update command, if package manager installation fails (e.g., due to peer dependency conflicts or network issues), the project's package.json is left in an updated state but node_modules is missing or inconsistent. To prevent leaving the workspace in a broken state, backup the original package.json before applying the update plan, and restore it from the backup if the installation tasks throw an error.
1 parent b0b3a47 commit d16c590

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

packages/angular/cli/src/commands/update/cli.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,14 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
541541
return 1;
542542
}
543543

544+
const packageJsonPath = path.join(this.context.root, 'package.json');
545+
let originalPackageJsonContent: string | undefined;
546+
try {
547+
originalPackageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
548+
} catch {
549+
// Ignore backup errors.
550+
}
551+
544552
try {
545553
await applyUpdatePlan(this.context.root, plan, logger);
546554
} catch (error) {
@@ -596,6 +604,16 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
596604
Module._pathCache = Object.create(null);
597605
}
598606
} catch (e) {
607+
if (originalPackageJsonContent !== undefined) {
608+
try {
609+
await fs.writeFile(packageJsonPath, originalPackageJsonContent, 'utf8');
610+
logger.info('Restored package.json to its original state.');
611+
} catch (restoreError) {
612+
assertIsError(restoreError);
613+
logger.error(`Failed to restore package.json: ${restoreError.message}`);
614+
}
615+
}
616+
599617
if (e instanceof CommandError) {
600618
return 1;
601619
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { readFile, writeFile } from 'node:fs/promises';
2+
import { createProjectFromAsset } from '../../utils/assets';
3+
import { getActivePackageManager } from '../../utils/packages';
4+
import { ng } from '../../utils/process';
5+
import { expectToFail } from '../../utils/utils';
6+
7+
export default async function () {
8+
let restoreRegistry: (() => Promise<void>) | undefined;
9+
try {
10+
restoreRegistry = await createProjectFromAsset('20.0-project', true);
11+
12+
// Read the original package.json content before running update.
13+
const originalPackageJson = await readFile('package.json', 'utf8');
14+
15+
// Force installation to fail by pointing the registry to an invalid URL.
16+
if (getActivePackageManager() === 'yarn') {
17+
await writeFile('.yarnrc.yml', 'npmRegistryServer: "http://registry.invalid"\n');
18+
} else {
19+
await writeFile('.npmrc', 'registry=http://registry.invalid\n');
20+
}
21+
22+
// Run update which is expected to fail during installation.
23+
const { message } = await expectToFail(() =>
24+
ng('update', '@angular/core', '@angular/cli', '--force', '--allow-dirty'),
25+
);
26+
27+
if (!message.includes('Restored package.json to its original state.')) {
28+
throw new Error(
29+
`Expected output to contain "Restored package.json to its original state." but didn't. OUTPUT: \n` +
30+
message,
31+
);
32+
}
33+
34+
// Verify that package.json was reverted and matches the original content.
35+
const currentPackageJson = await readFile('package.json', 'utf8');
36+
if (currentPackageJson !== originalPackageJson) {
37+
throw new Error(
38+
`package.json was not restored to its original state.\n` +
39+
`Expected:\n${originalPackageJson}\n\n` +
40+
`Actual:\n${currentPackageJson}`,
41+
);
42+
}
43+
} finally {
44+
await restoreRegistry?.();
45+
}
46+
}

0 commit comments

Comments
 (0)