Skip to content

Commit b0dcf00

Browse files
committed
refactor(@angular/cli): move update version resolution directly to CLI command
Removes the @schematics/update:update schematic and moves the package version resolution, group expansion, and peer dependency validation logic directly into the CLI's update command. This simplifies the command execution flow, eliminates sharing state via global variables, and enables direct unit testing of the resolution plan in isolated temporary directories without host monorepo package leakage.
1 parent 167aea1 commit b0dcf00

8 files changed

Lines changed: 1373 additions & 1739 deletions

File tree

packages/angular/cli/BUILD.bazel

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ RUNTIME_ASSETS = glob(
2929
include = [
3030
"bin/**/*",
3131
"src/**/*.md",
32-
"src/**/*.json",
3332
],
3433
exclude = [
3534
"lib/config/workspace-schema.json",
@@ -53,7 +52,6 @@ ts_project(
5352
) + [
5453
# These files are generated from the JSON schema
5554
"//packages/angular/cli:lib/config/workspace-schema.ts",
56-
"//packages/angular/cli:src/commands/update/schematic/schema.ts",
5755
],
5856
data = RUNTIME_ASSETS,
5957
deps = [
@@ -105,11 +103,6 @@ ts_json_schema(
105103
data = CLI_SCHEMA_DATA,
106104
)
107105

108-
ts_json_schema(
109-
name = "update_schematic_schema",
110-
src = "src/commands/update/schematic/schema.json",
111-
)
112-
113106
ts_project(
114107
name = "angular-cli_test_lib",
115108
testonly = True,

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

Lines changed: 115 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import type { InstalledPackage, PackageManager, PackageManifest } from '../../pa
2424
import { colors } from '../../utilities/color';
2525
import { disableVersionCheck } from '../../utilities/environment-options';
2626
import { assertIsError } from '../../utilities/error';
27+
import {
28+
UpdatePlan,
29+
applyUpdatePlan,
30+
findPackageJson,
31+
printUpdateUsageMessage,
32+
resolveUserUpdatePlan,
33+
} from './update-resolver';
2734
import {
2835
checkCLIVersion,
2936
coerceVersionNumber,
@@ -32,12 +39,7 @@ import {
3239
} from './utilities/cli-version';
3340
import { ANGULAR_PACKAGES_REGEXP } from './utilities/constants';
3441
import { checkCleanGit } from './utilities/git';
35-
import {
36-
commitChanges,
37-
executeMigration,
38-
executeMigrations,
39-
executeSchematic,
40-
} from './utilities/migration';
42+
import { commitChanges, executeMigration, executeMigrations } from './utilities/migration';
4143

4244
interface UpdateCommandArgs {
4345
packages?: string[];
@@ -54,8 +56,6 @@ interface UpdateCommandArgs {
5456

5557
class CommandError extends Error {}
5658

57-
const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json');
58-
5959
export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
6060
override scope = CommandScope.In;
6161
protected override shouldReportAnalytics = false;
@@ -244,23 +244,28 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
244244
});
245245

246246
if (packages.length === 0) {
247-
// Show status
248-
const { success } = await executeSchematic(
249-
workflow,
250-
logger,
251-
UPDATE_SCHEMATIC_COLLECTION,
252-
'update',
253-
{
254-
force: options.force,
255-
next: options.next,
256-
verbose: options.verbose,
257-
packageManager: packageManager.name,
258-
packages: [],
259-
workspaceRoot: this.context.root,
260-
},
261-
);
247+
try {
248+
const plan = await resolveUserUpdatePlan(
249+
{
250+
force: options.force,
251+
next: options.next,
252+
verbose: options.verbose,
253+
packageManager: packageManager.name,
254+
packages: [],
255+
workspaceRoot: this.context.root,
256+
},
257+
logger,
258+
);
259+
260+
printUpdateUsageMessage(plan.packageInfoMap, logger, options.next);
262261

263-
return success ? 0 : 1;
262+
return 0;
263+
} catch (error) {
264+
assertIsError(error);
265+
logger.error(error.message);
266+
267+
return 1;
268+
}
264269
}
265270

266271
return options.migrateOnly
@@ -509,128 +514,113 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
509514
return 0;
510515
}
511516

512-
const { success } = await executeSchematic(
513-
workflow,
514-
logger,
515-
UPDATE_SCHEMATIC_COLLECTION,
516-
'update',
517-
{
518-
verbose: options.verbose,
519-
force: options.force,
520-
next: options.next,
521-
packageManager: this.context.packageManager.name,
522-
packages: packagesToUpdate,
523-
workspaceRoot: this.context.root,
524-
},
525-
);
526-
527-
if (success) {
528-
const { root: commandRoot } = this.context;
529-
const ignorePeerDependencies = await shouldForcePackageManager(
530-
packageManager,
517+
let plan: UpdatePlan;
518+
try {
519+
plan = await resolveUserUpdatePlan(
520+
{
521+
packages: packagesToUpdate,
522+
force: options.force,
523+
next: options.next,
524+
packageManager: packageManager.name,
525+
verbose: options.verbose,
526+
workspaceRoot: this.context.root,
527+
},
531528
logger,
532-
options.verbose,
533529
);
534-
const tasks = new Listr([
535-
{
536-
title: 'Cleaning node modules directory',
537-
async task(_, task) {
538-
try {
539-
await fs.rm(path.join(commandRoot, 'node_modules'), {
540-
force: true,
541-
recursive: true,
542-
maxRetries: 3,
543-
});
544-
} catch (e) {
545-
assertIsError(e);
546-
if (e.code === 'ENOENT') {
547-
task.skip('Cleaning not required. Node modules directory not found.');
548-
}
530+
} catch (error) {
531+
assertIsError(error);
532+
logger.error(error.message);
533+
534+
return 1;
535+
}
536+
537+
try {
538+
await applyUpdatePlan(this.context.root, plan, logger);
539+
} catch (error) {
540+
assertIsError(error);
541+
logger.error(`Error updating package.json: ${error.message}`);
542+
543+
return 1;
544+
}
545+
546+
const { root: commandRoot } = this.context;
547+
const ignorePeerDependencies = await shouldForcePackageManager(
548+
packageManager,
549+
logger,
550+
options.verbose,
551+
);
552+
const tasks = new Listr([
553+
{
554+
title: 'Cleaning node modules directory',
555+
async task(_, task) {
556+
try {
557+
await fs.rm(path.join(commandRoot, 'node_modules'), {
558+
force: true,
559+
recursive: true,
560+
maxRetries: 3,
561+
});
562+
} catch (e) {
563+
assertIsError(e);
564+
if (e.code === 'ENOENT') {
565+
task.skip('Cleaning not required. Node modules directory not found.');
549566
}
550-
},
567+
}
551568
},
552-
{
553-
title: 'Installing packages',
554-
async task() {
555-
try {
556-
await packageManager.install({
557-
ignorePeerDependencies,
558-
});
559-
} catch (e) {
560-
throw new CommandError('Unable to install packages');
561-
}
562-
},
569+
},
570+
{
571+
title: 'Installing packages',
572+
async task() {
573+
try {
574+
await packageManager.install({
575+
ignorePeerDependencies,
576+
});
577+
} catch (e) {
578+
throw new CommandError('Unable to install packages');
579+
}
563580
},
564-
]);
565-
try {
566-
await tasks.run();
567-
} catch (e) {
568-
if (e instanceof CommandError) {
569-
return 1;
570-
}
571-
572-
throw e;
581+
},
582+
]);
583+
try {
584+
await tasks.run();
585+
// Clear Node's module resolution path cache to prevent stale lookups
586+
// when resolving migration package paths.
587+
const Module = require('node:module');
588+
if (Module && Module._pathCache) {
589+
Module._pathCache = Object.create(null);
573590
}
591+
} catch (e) {
592+
if (e instanceof CommandError) {
593+
return 1;
594+
}
595+
596+
throw e;
574597
}
575598

576-
if (success && options.createCommits) {
599+
if (options.createCommits) {
577600
if (
578601
!commitChanges(logger, `Angular CLI update for packages - ${packagesToUpdate.join(', ')}`)
579602
) {
580603
return 1;
581604
}
582605
}
583606

584-
// This is a temporary workaround to allow data to be passed back from the update schematic
585-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
586-
const migrations = (global as any).externalMigrations as {
587-
package: string;
588-
collection: string;
589-
from: string;
590-
to: string;
591-
}[];
592-
593-
if (success && migrations) {
594-
const rootRequire = createRequire(this.context.root + '/');
607+
const migrations = plan.migrationsToRun;
608+
609+
if (migrations) {
595610
for (const migration of migrations) {
596611
// Resolve the package from the workspace root, as otherwise it will be resolved from the temp
597612
// installed CLI version.
598-
let packagePath;
599-
logVerbose(
600-
`Resolving migration package '${migration.package}' from '${this.context.root}'...`,
601-
);
602-
try {
603-
try {
604-
packagePath = path.dirname(
605-
// This may fail if the `package.json` is not exported as an entry point
606-
rootRequire.resolve(path.join(migration.package, 'package.json')),
607-
);
608-
} catch (e) {
609-
assertIsError(e);
610-
if (e.code === 'MODULE_NOT_FOUND') {
611-
// Fallback to trying to resolve the package's main entry point
612-
packagePath = rootRequire.resolve(migration.package);
613-
} else {
614-
throw e;
615-
}
616-
}
617-
} catch (e) {
618-
assertIsError(e);
619-
if (e.code === 'MODULE_NOT_FOUND') {
620-
logVerbose(e.toString());
621-
logger.error(
622-
`Migrations for package (${migration.package}) were not found.` +
623-
' The package could not be found in the workspace.',
624-
);
625-
} else {
626-
logger.error(
627-
`Unable to resolve migrations for package (${migration.package}). [${e.message}]`,
628-
);
629-
}
613+
const packageJsonPath = findPackageJson(this.context.root, migration.package);
614+
if (!packageJsonPath) {
615+
logger.error(
616+
`Migrations for package (${migration.package}) were not found.` +
617+
' The package could not be found in the workspace.',
618+
);
630619

631620
return 1;
632621
}
633622

623+
const packagePath = path.dirname(packageJsonPath);
634624
let migrations;
635625

636626
// Check if it is a package-local location
@@ -673,7 +663,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
673663
}
674664
}
675665

676-
return success ? 0 : 1;
666+
return 0;
677667
}
678668
}
679669

@@ -686,14 +676,3 @@ async function readPackageManifest(manifestPath: string): Promise<PackageManifes
686676
return undefined;
687677
}
688678
}
689-
690-
function findPackageJson(workspaceDir: string, packageName: string): string | undefined {
691-
try {
692-
const projectRequire = createRequire(path.join(workspaceDir, 'package.json'));
693-
const packageJsonPath = projectRequire.resolve(`${packageName}/package.json`);
694-
695-
return packageJsonPath;
696-
} catch {
697-
return undefined;
698-
}
699-
}

packages/angular/cli/src/commands/update/schematic/collection.json

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)