in packages/angular/cli/commands/update-impl.ts [269:772]
async run(options: UpdateCommandSchema & Arguments) {
await ensureCompatibleNpm(this.context.root);
// Check if the current installed CLI version is older than the latest compatible version.
if (!disableVersionCheck) {
const cliVersionToInstall = await this.checkCLIVersion(
options['--'],
options.verbose,
options.next,
);
if (cliVersionToInstall) {
this.logger.warn(
'The installed Angular CLI version is outdated.\n' +
`Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`,
);
return runTempPackageBin(
`@angular/cli@${cliVersionToInstall}`,
this.packageManager,
process.argv.slice(2),
);
}
}
const logVerbose = (message: string) => {
if (options.verbose) {
this.logger.info(message);
}
};
const packages: PackageIdentifier[] = [];
for (const request of options['--'] || []) {
try {
const packageIdentifier = npa(request);
// only registry identifiers are supported
if (!packageIdentifier.registry) {
this.logger.error(`Package '${request}' is not a registry package identifer.`);
return 1;
}
if (packages.some((v) => v.name === packageIdentifier.name)) {
this.logger.error(`Duplicate package '${packageIdentifier.name}' specified.`);
return 1;
}
if (options.migrateOnly && packageIdentifier.rawSpec) {
this.logger.warn('Package specifier has no effect when using "migrate-only" option.');
}
// If next option is used and no specifier supplied, use next tag
if (options.next && !packageIdentifier.rawSpec) {
packageIdentifier.fetchSpec = 'next';
}
packages.push(packageIdentifier as PackageIdentifier);
} catch (e) {
this.logger.error(e.message);
return 1;
}
}
if (!options.migrateOnly && (options.from || options.to)) {
this.logger.error('Can only use "from" or "to" options with "migrate-only" option.');
return 1;
}
// If not asking for status then check for a clean git repository.
// This allows the user to easily reset any changes from the update.
if (packages.length && !this.checkCleanGit()) {
if (options.allowDirty) {
this.logger.warn(
'Repository is not clean. Update changes will be mixed with pre-existing changes.',
);
} else {
this.logger.error(
'Repository is not clean. Please commit or stash any changes before updating.',
);
return 2;
}
}
this.logger.info(`Using package manager: '${this.packageManager}'`);
this.logger.info('Collecting installed dependencies...');
const rootDependencies = await getProjectDependencies(this.context.root);
this.logger.info(`Found ${rootDependencies.size} dependencies.`);
if (packages.length === 0) {
// Show status
const { success } = await this.executeSchematic(UPDATE_SCHEMATIC_COLLECTION, 'update', {
force: options.force || false,
next: options.next || false,
verbose: options.verbose || false,
packageManager: this.packageManager,
packages: [],
});
return success ? 0 : 1;
}
if (options.migrateOnly) {
if (!options.from && typeof options.migrateOnly !== 'string') {
this.logger.error(
'"from" option is required when using the "migrate-only" option without a migration name.',
);
return 1;
} else if (packages.length !== 1) {
this.logger.error(
'A single package must be specified when using the "migrate-only" option.',
);
return 1;
}
if (options.next) {
this.logger.warn('"next" option has no effect when using "migrate-only" option.');
}
const packageName = packages[0].name;
const packageDependency = rootDependencies.get(packageName);
let packagePath = packageDependency?.path;
let packageNode = packageDependency?.package;
if (packageDependency && !packageNode) {
this.logger.error('Package found in package.json but is not installed.');
return 1;
} else if (!packageDependency) {
// Allow running migrations on transitively installed dependencies
// There can technically be nested multiple versions
// TODO: If multiple, this should find all versions and ask which one to use
const packageJson = findPackageJson(this.context.root, packageName);
if (packageJson) {
packagePath = path.dirname(packageJson);
packageNode = await readPackageJson(packageJson);
}
}
if (!packageNode || !packagePath) {
this.logger.error('Package is not installed.');
return 1;
}
const updateMetadata = packageNode['ng-update'];
let migrations = updateMetadata?.migrations;
if (migrations === undefined) {
this.logger.error('Package does not provide migrations.');
return 1;
} else if (typeof migrations !== 'string') {
this.logger.error('Package contains a malformed migrations field.');
return 1;
} else if (path.posix.isAbsolute(migrations) || path.win32.isAbsolute(migrations)) {
this.logger.error(
'Package contains an invalid migrations field. Absolute paths are not permitted.',
);
return 1;
}
// Normalize slashes
migrations = migrations.replace(/\\/g, '/');
if (migrations.startsWith('../')) {
this.logger.error(
'Package contains an invalid migrations field. Paths outside the package root are not permitted.',
);
return 1;
}
// Check if it is a package-local location
const localMigrations = path.join(packagePath, migrations);
if (fs.existsSync(localMigrations)) {
migrations = localMigrations;
} else {
// Try to resolve from package location.
// This avoids issues with package hoisting.
try {
migrations = require.resolve(migrations, { paths: [packagePath] });
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
this.logger.error('Migrations for package were not found.');
} else {
this.logger.error(`Unable to resolve migrations for package. [${e.message}]`);
}
return 1;
}
}
let result: boolean;
if (typeof options.migrateOnly == 'string') {
result = await this.executeMigration(
packageName,
migrations,
options.migrateOnly,
options.createCommits,
);
} else {
const from = coerceVersionNumber(options.from);
if (!from) {
this.logger.error(`"from" value [${options.from}] is not a valid version.`);
return 1;
}
result = await this.executeMigrations(
packageName,
migrations,
from,
options.to || packageNode.version,
options.createCommits,
);
}
return result ? 0 : 1;
}
const requests: {
identifier: PackageIdentifier;
node: PackageTreeNode;
}[] = [];
// Validate packages actually are part of the workspace
for (const pkg of packages) {
const node = rootDependencies.get(pkg.name);
if (!node?.package) {
this.logger.error(`Package '${pkg.name}' is not a dependency.`);
return 1;
}
// If a specific version is requested and matches the installed version, skip.
if (pkg.type === 'version' && node.package.version === pkg.fetchSpec) {
this.logger.info(`Package '${pkg.name}' is already at '${pkg.fetchSpec}'.`);
continue;
}
requests.push({ identifier: pkg, node });
}
if (requests.length === 0) {
return 0;
}
const packagesToUpdate: string[] = [];
this.logger.info('Fetching dependency metadata from registry...');
for (const { identifier: requestIdentifier, node } of requests) {
const packageName = requestIdentifier.name;
let metadata;
try {
// Metadata requests are internally cached; multiple requests for same name
// does not result in additional network traffic
metadata = await fetchPackageMetadata(packageName, this.logger, {
verbose: options.verbose,
});
} catch (e) {
this.logger.error(`Error fetching metadata for '${packageName}': ` + e.message);
return 1;
}
// Try to find a package version based on the user requested package specifier
// registry specifier types are either version, range, or tag
let manifest: PackageManifest | undefined;
if (
requestIdentifier.type === 'version' ||
requestIdentifier.type === 'range' ||
requestIdentifier.type === 'tag'
) {
try {
manifest = pickManifest(metadata, requestIdentifier.fetchSpec);
} catch (e) {
if (e.code === 'ETARGET') {
// If not found and next was used and user did not provide a specifier, try latest.
// Package may not have a next tag.
if (
requestIdentifier.type === 'tag' &&
requestIdentifier.fetchSpec === 'next' &&
!requestIdentifier.rawSpec
) {
try {
manifest = pickManifest(metadata, 'latest');
} catch (e) {
if (e.code !== 'ETARGET' && e.code !== 'ENOVERSIONS') {
throw e;
}
}
}
} else if (e.code !== 'ENOVERSIONS') {
throw e;
}
}
}
if (!manifest) {
this.logger.error(
`Package specified by '${requestIdentifier.raw}' does not exist within the registry.`,
);
return 1;
}
if (manifest.version === node.package?.version) {
this.logger.info(`Package '${packageName}' is already up to date.`);
continue;
}
if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) {
const { name, version } = node.package;
const toBeInstalledMajorVersion = +manifest.version.split('.')[0];
const currentMajorVersion = +version.split('.')[0];
if (toBeInstalledMajorVersion - currentMajorVersion > 1) {
// Only allow updating a single version at a time.
if (currentMajorVersion < 6) {
// Before version 6, the major versions were not always sequential.
// Example @angular/core skipped version 3, @angular/cli skipped versions 2-5.
this.logger.error(
`Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
`For more information about the update process, see https://update.angular.io/.`,
);
} else {
const nextMajorVersionFromCurrent = currentMajorVersion + 1;
this.logger.error(
`Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
`Run 'ng update ${name}@${nextMajorVersionFromCurrent}' in your workspace directory ` +
`to update to latest '${nextMajorVersionFromCurrent}.x' version of '${name}'.\n\n` +
`For more information about the update process, see https://update.angular.io/?v=${currentMajorVersion}.0-${nextMajorVersionFromCurrent}.0`,
);
}
return 1;
}
}
packagesToUpdate.push(requestIdentifier.toString());
}
if (packagesToUpdate.length === 0) {
return 0;
}
const { success } = await this.executeSchematic(UPDATE_SCHEMATIC_COLLECTION, 'update', {
verbose: options.verbose || false,
force: options.force || false,
next: !!options.next,
packageManager: this.packageManager,
packages: packagesToUpdate,
});
if (success) {
try {
// Remove existing node modules directory to provide a stronger guarantee that packages
// will be hoisted into the correct locations.
// The below should be removed and replaced with just `rm` when support for Node.Js 12 is removed.
const { rm, rmdir } = fs.promises as typeof fs.promises & {
rm?: (
path: fs.PathLike,
options?: {
force?: boolean;
maxRetries?: number;
recursive?: boolean;
retryDelay?: number;
},
) => Promise<void>;
};
if (rm) {
await rm(path.join(this.context.root, 'node_modules'), {
force: true,
recursive: true,
maxRetries: 3,
});
} else {
await rmdir(path.join(this.context.root, 'node_modules'), {
recursive: true,
maxRetries: 3,
});
}
} catch {}
const result = await installAllPackages(
this.packageManager,
options.force ? ['--force'] : [],
this.context.root,
);
if (result !== 0) {
return result;
}
}
if (success && options.createCommits) {
const committed = this.commit(
`Angular CLI update for packages - ${packagesToUpdate.join(', ')}`,
);
if (!committed) {
return 1;
}
}
// This is a temporary workaround to allow data to be passed back from the update schematic
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const migrations = (global as any).externalMigrations as {
package: string;
collection: string;
from: string;
to: string;
}[];
if (success && migrations) {
for (const migration of migrations) {
// Resolve the package from the workspace root, as otherwise it will be resolved from the temp
// installed CLI version.
let packagePath;
logVerbose(
`Resolving migration package '${migration.package}' from '${this.context.root}'...`,
);
try {
try {
packagePath = path.dirname(
// This may fail if the `package.json` is not exported as an entry point
require.resolve(path.join(migration.package, 'package.json'), {
paths: [this.context.root],
}),
);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
// Fallback to trying to resolve the package's main entry point
packagePath = require.resolve(migration.package, { paths: [this.context.root] });
} else {
throw e;
}
}
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
logVerbose(e.toString());
this.logger.error(
`Migrations for package (${migration.package}) were not found.` +
' The package could not be found in the workspace.',
);
} else {
this.logger.error(
`Unable to resolve migrations for package (${migration.package}). [${e.message}]`,
);
}
return 1;
}
let migrations;
// Check if it is a package-local location
const localMigrations = path.join(packagePath, migration.collection);
if (fs.existsSync(localMigrations)) {
migrations = localMigrations;
} else {
// Try to resolve from package location.
// This avoids issues with package hoisting.
try {
migrations = require.resolve(migration.collection, { paths: [packagePath] });
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
this.logger.error(`Migrations for package (${migration.package}) were not found.`);
} else {
this.logger.error(
`Unable to resolve migrations for package (${migration.package}). [${e.message}]`,
);
}
return 1;
}
}
const result = await this.executeMigrations(
migration.package,
migrations,
migration.from,
migration.to,
options.createCommits,
);
if (!result) {
return 0;
}
}
}
return success ? 0 : 1;
}