Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 100 additions & 16 deletions packages/angular/cli/src/commands/update/update-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,23 @@ async function _buildPackageInfo(
};
}

function splitPackageName(pkg: string): { name: string; version?: string } {
let name = pkg;
let version: string | undefined;

if (pkg.startsWith('@')) {
const parts = pkg.split('@');
name = '@' + parts[1];
version = parts[2];
} else if (pkg.includes('@')) {
const parts = pkg.split('@');
name = parts[0];
version = parts[1];
}

return { name, version };
}

function _buildPackageList(
options: UpdateResolverOptions,
allDependencies: ReadonlyMap<string, VersionRange>,
Expand All @@ -554,28 +571,18 @@ function _buildPackageList(
}

for (const pkg of inputPackages) {
let pkgName = pkg;
let pkgVersion: string | undefined;

if (pkg.startsWith('@')) {
const parts = pkg.split('@');
pkgName = '@' + parts[1];
pkgVersion = parts[2];
} else if (pkg.includes('@')) {
const parts = pkg.split('@');
pkgName = parts[0];
pkgVersion = parts[1];
}
const { name: pkgName, version: pkgVersion } = splitPackageName(pkg);

if (!allDependencies.has(pkgName)) {
throw new Error(`Package ${JSON.stringify(pkgName)} is not in package.json.`);
}

if (options.migrateOnly && !pkgVersion && options.from) {
pkgVersion = options.from;
let targetVersion = pkgVersion;
if (options.migrateOnly && !targetVersion && options.from) {
targetVersion = options.from;
}

packages.set(pkgName, (pkgVersion || (options.next ? 'next' : 'latest')) as VersionRange);
packages.set(pkgName, (targetVersion || (options.next ? 'next' : 'latest')) as VersionRange);
}

return packages;
Expand Down Expand Up @@ -759,6 +766,74 @@ function isPkgFromRegistry(name: string, specifier: string): boolean {
return !!result.registry;
}

async function checkCatalogUpdates(
normalizedPackages: string[],
packageJsonContent: PackageManifest,
registryClient: RegistryClient,
workspaceRoot: string,
options: UpdateResolverOptions,
): Promise<void> {
const catalogUpdates: { name: string; current: string; target: string; specifier: string }[] = [];

for (const requestedPkg of normalizedPackages) {
const { name: pkgName } = splitPackageName(requestedPkg);
const specifier =
packageJsonContent.dependencies?.[pkgName] ||
packageJsonContent.devDependencies?.[pkgName] ||
packageJsonContent.peerDependencies?.[pkgName];

if (specifier?.startsWith('catalog:')) {
const current = getInstalledVersion(pkgName, workspaceRoot) ?? 'unknown';
let target = 'latest';
try {
const metadata = await registryClient.getMetadata(pkgName);
if (metadata) {
const resolved = await resolvePackageVersion(
registryClient,
metadata,
options.next ? 'next' : 'latest',
!!options.next,
);
target = resolved ?? 'latest';
}
} catch {
// Fallback to 'latest' tag
}

catalogUpdates.push({ name: pkgName, current, target, specifier });
}
}
Comment thread
clydin marked this conversation as resolved.

if (catalogUpdates.length > 0) {
const packageManagerName = options.packageManager ?? 'your package manager';
const installCmd = packageManagerName === 'yarn' ? 'yarn install' : 'pnpm install';

const updatesList = catalogUpdates
.map((pkg) => ` - ${pkg.name} (${pkg.specifier}) -> Target version: ${pkg.target}`)
.join('\n');

const migrationCommands = catalogUpdates
.map((pkg) => {
const fromVer = pkg.current === 'unknown' ? '<current-version>' : pkg.current;

return ` ng update ${pkg.name} --migrate-only --from ${fromVer}`;
})
.join('\n');

throw new Error(
`The following packages to update are configured to use catalogs:\n` +
`${updatesList}\n\n` +
`Because catalogs are shared across the monorepo, 'ng update' cannot modify them directly.\n` +
`Please perform the following steps to update:\n` +
` 1. Manually update the versions for these packages in your catalog configuration file ` +
`(e.g., pnpm-workspace.yaml or .yarnrc.yml).\n` +
` 2. Run '${installCmd}' to install the updated versions.\n` +
` 3. Run the following command(s) to execute the migration schematics:\n` +
`${migrationCommands}`,
);
}
}

export async function resolveUserUpdatePlan(
options: UpdateResolverOptions,
packageManager: PackageManager,
Expand Down Expand Up @@ -810,10 +885,19 @@ export async function resolveUserUpdatePlan(
options.to = _formatVersion(options.to);
const usingYarn = options.packageManager === 'yarn';

const packages = _buildPackageList(options, npmDeps, logger);
const minReleaseAge = await packageManager.getMinimumReleaseAge();
const registryClient = new RegistryClient(packageManager, logger, minReleaseAge);

await checkCatalogUpdates(
normalizedPackages,
packageJsonContent,
registryClient,
workspaceRoot,
options,
);

const packages = _buildPackageList(options, npmDeps, logger);

const getOrFetchPackageMetadata = async (
packageName: string,
): Promise<PackageMetadata | null> => {
Expand Down
68 changes: 68 additions & 0 deletions packages/angular/cli/src/commands/update/update-resolver_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,74 @@ describe('UpdateResolver', () => {
expect(plan.packagesToUpdate.get('@angular-devkit-tests/common-bug')).toBe('13.2.0');
expect(plan.packagesToUpdate.get('@angular-devkit-tests/cdk-bug')).toBe('13.2.0');
});

it('rejects updates for catalog packages and guides on manual migration steps', async () => {
createMockWorkspace(
{
name: 'blah',
dependencies: {
'@angular/core': 'catalog:',
},
},
{
'@angular/core': { version: '5.1.0' },
},
);

const expectedError = [
`The following packages to update are configured to use catalogs:`,
`\\s+- @angular/core \\(catalog:\\) -> Target version: 6\\.0\\.0`,
``,
`Because catalogs are shared across the monorepo, 'ng update' cannot modify them directly\\.`,
`Please perform the following steps to update:`,
`\\s+1\\. Manually update the versions for these packages in your catalog configuration file ` +
`\\(e\\.g\\., pnpm-workspace\\.yaml or \\.yarnrc\\.yml\\)\\.`,
`\\s+2\\. Run 'pnpm install' to install the updated versions\\.`,
`\\s+3\\. Run the following command\\(s\\) to execute the migration schematics:`,
`\\s+ng update @angular/core --migrate-only --from 5\\.1\\.0`,
].join('\\n');

await expectAsync(
resolvePlan({
packages: ['@angular/core'],
workspaceRoot: tempRoot,
}),
).toBeRejectedWithError(new RegExp(expectedError));
});

it('rejects updates for catalog packages specified with a version suffix', async () => {
createMockWorkspace(
{
name: 'blah',
dependencies: {
'@angular/core': 'catalog:',
},
},
{
'@angular/core': { version: '5.1.0' },
},
);

const expectedError = [
`The following packages to update are configured to use catalogs:`,
`\\s+- @angular/core \\(catalog:\\) -> Target version: 6\\.0\\.0`,
``,
`Because catalogs are shared across the monorepo, 'ng update' cannot modify them directly\\.`,
`Please perform the following steps to update:`,
`\\s+1\\. Manually update the versions for these packages in your catalog configuration file ` +
`\\(e\\.g\\., pnpm-workspace\\.yaml or \\.yarnrc\\.yml\\)\\.`,
`\\s+2\\. Run 'pnpm install' to install the updated versions\\.`,
`\\s+3\\. Run the following command\\(s\\) to execute the migration schematics:`,
`\\s+ng update @angular/core --migrate-only --from 5\\.1\\.0`,
].join('\\n');

await expectAsync(
resolvePlan({
packages: ['@angular/core@6'],
workspaceRoot: tempRoot,
}),
).toBeRejectedWithError(new RegExp(expectedError));
});
});

describe('RegistryClient', () => {
Expand Down
Loading