UNPKG

@angular/cli

Version:
918 lines (917 loc) • 44 kB
"use strict"; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const schematics_1 = require("@angular-devkit/schematics"); const tools_1 = require("@angular-devkit/schematics/tools"); const listr2_1 = require("listr2"); const node_child_process_1 = require("node:child_process"); const node_fs_1 = require("node:fs"); const node_module_1 = require("node:module"); const path = __importStar(require("node:path")); const node_path_1 = require("node:path"); const npm_package_arg_1 = __importDefault(require("npm-package-arg")); const npm_pick_manifest_1 = __importDefault(require("npm-pick-manifest")); const semver = __importStar(require("semver")); const workspace_schema_1 = require("../../../lib/config/workspace-schema"); const command_module_1 = require("../../command-builder/command-module"); const schematic_engine_host_1 = require("../../command-builder/utilities/schematic-engine-host"); const schematic_workflow_1 = require("../../command-builder/utilities/schematic-workflow"); const color_1 = require("../../utilities/color"); const environment_options_1 = require("../../utilities/environment-options"); const error_1 = require("../../utilities/error"); const log_file_1 = require("../../utilities/log-file"); const package_metadata_1 = require("../../utilities/package-metadata"); const package_tree_1 = require("../../utilities/package-tree"); const prompt_1 = require("../../utilities/prompt"); const tty_1 = require("../../utilities/tty"); const version_1 = require("../../utilities/version"); class CommandError extends Error { } const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//; const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json'); class UpdateCommandModule extends command_module_1.CommandModule { scope = command_module_1.CommandScope.In; shouldReportAnalytics = false; resolvePaths = [__dirname, this.context.root]; command = 'update [packages..]'; describe = 'Updates your workspace and its dependencies. See https://update.angular.dev/.'; longDescriptionPath = (0, node_path_1.join)(__dirname, 'long-description.md'); builder(localYargs) { return localYargs .positional('packages', { description: 'The names of package(s) to update.', type: 'string', array: true, }) .option('force', { description: 'Ignore peer dependency version mismatches.', type: 'boolean', default: false, }) .option('next', { description: 'Use the prerelease version, including beta and RCs.', type: 'boolean', default: false, }) .option('migrate-only', { description: 'Only perform a migration, do not update the installed version.', type: 'boolean', }) .option('name', { description: 'The name of the migration to run. Only available when a single package is updated.', type: 'string', conflicts: ['to', 'from'], }) .option('from', { description: 'Version from which to migrate from. ' + `Only available when a single package is updated, and only with 'migrate-only'.`, type: 'string', implies: ['migrate-only'], conflicts: ['name'], }) .option('to', { describe: 'Version up to which to apply migrations. Only available when a single package is updated, ' + `and only with 'migrate-only' option. Requires 'from' to be specified. Default to the installed version detected.`, type: 'string', implies: ['from', 'migrate-only'], conflicts: ['name'], }) .option('allow-dirty', { describe: 'Whether to allow updating when the repository contains modified or untracked files.', type: 'boolean', default: false, }) .option('verbose', { describe: 'Display additional details about internal operations during execution.', type: 'boolean', default: false, }) .option('create-commits', { describe: 'Create source control commits for updates and migrations.', type: 'boolean', alias: ['C'], default: false, }) .middleware((argv) => { if (argv.name) { argv['migrate-only'] = true; } // eslint-disable-next-line @typescript-eslint/no-explicit-any return argv; }) .check(({ packages, 'allow-dirty': allowDirty, 'migrate-only': migrateOnly }) => { const { logger } = this.context; // This allows the user to easily reset any changes from the update. if (packages?.length && !this.checkCleanGit()) { if (allowDirty) { logger.warn('Repository is not clean. Update changes will be mixed with pre-existing changes.'); } else { throw new command_module_1.CommandModuleError('Repository is not clean. Please commit or stash any changes before updating.'); } } if (migrateOnly) { if (packages?.length !== 1) { throw new command_module_1.CommandModuleError(`A single package must be specified when using the 'migrate-only' option.`); } } return true; }) .strict(); } async run(options) { const { logger, packageManager } = this.context; // Check if the current installed CLI version is older than the latest compatible version. // Skip when running `ng update` without a package name as this will not trigger an actual update. if (!environment_options_1.disableVersionCheck && options.packages?.length) { const cliVersionToInstall = await this.checkCLIVersion(options.packages, options.verbose, options.next); if (cliVersionToInstall) { logger.warn('The installed Angular CLI version is outdated.\n' + `Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`); return this.runTempBinary(`@angular/cli@${cliVersionToInstall}`, process.argv.slice(2)); } } const packages = []; for (const request of options.packages ?? []) { try { const packageIdentifier = (0, npm_package_arg_1.default)(request); // only registry identifiers are supported if (!packageIdentifier.registry) { logger.error(`Package '${request}' is not a registry package identifer.`); return 1; } if (packages.some((v) => v.name === packageIdentifier.name)) { logger.error(`Duplicate package '${packageIdentifier.name}' specified.`); return 1; } if (options.migrateOnly && packageIdentifier.rawSpec !== '*') { 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); } catch (e) { (0, error_1.assertIsError)(e); logger.error(e.message); return 1; } } logger.info(`Using package manager: ${color_1.colors.gray(packageManager.name)}`); logger.info('Collecting installed dependencies...'); const rootDependencies = await (0, package_tree_1.getProjectDependencies)(this.context.root); logger.info(`Found ${rootDependencies.size} dependencies.`); const workflow = new tools_1.NodeWorkflow(this.context.root, { packageManager: packageManager.name, packageManagerForce: this.packageManagerForce(options.verbose), // __dirname -> favor @schematics/update from this package // Otherwise, use packages from the active workspace (migrations) resolvePaths: this.resolvePaths, schemaValidation: true, engineHostCreator: (options) => new schematic_engine_host_1.SchematicEngineHost(options.resolvePaths), }); if (packages.length === 0) { // Show status const { success } = await this.executeSchematic(workflow, UPDATE_SCHEMATIC_COLLECTION, 'update', { force: options.force, next: options.next, verbose: options.verbose, packageManager: packageManager.name, packages: [], }); return success ? 0 : 1; } return options.migrateOnly ? this.migrateOnly(workflow, (options.packages ?? [])[0], rootDependencies, options) : this.updatePackagesAndMigrate(workflow, rootDependencies, options, packages); } async executeSchematic(workflow, collection, schematic, options = {}) { const { logger } = this.context; const workflowSubscription = (0, schematic_workflow_1.subscribeToWorkflow)(workflow, logger); // TODO: Allow passing a schematic instance directly try { await workflow .execute({ collection, schematic, options, logger, }) .toPromise(); return { success: !workflowSubscription.error, files: workflowSubscription.files }; } catch (e) { if (e instanceof schematics_1.UnsuccessfulWorkflowExecution) { logger.error(`${color_1.figures.cross} Migration failed. See above for further details.\n`); } else { (0, error_1.assertIsError)(e); const logPath = (0, log_file_1.writeErrorToLogFile)(e); logger.fatal(`${color_1.figures.cross} Migration failed: ${e.message}\n` + ` See "${logPath}" for further details.\n`); } return { success: false, files: workflowSubscription.files }; } finally { workflowSubscription.unsubscribe(); } } /** * @return Whether or not the migration was performed successfully. */ async executeMigration(workflow, packageName, collectionPath, migrationName, commit) { const { logger } = this.context; const collection = workflow.engine.createCollection(collectionPath); const name = collection.listSchematicNames().find((name) => name === migrationName); if (!name) { logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`); return 1; } logger.info(color_1.colors.cyan(`** Executing '${migrationName}' of package '${packageName}' **\n`)); const schematic = workflow.engine.createSchematic(name, collection); return this.executePackageMigrations(workflow, [schematic.description], packageName, commit); } /** * @return Whether or not the migrations were performed successfully. */ async executeMigrations(workflow, packageName, collectionPath, from, to, commit) { const collection = workflow.engine.createCollection(collectionPath); const migrationRange = new semver.Range('>' + (semver.prerelease(from) ? from.split('-')[0] + '-0' : from) + ' <=' + to.split('-')[0]); const requiredMigrations = []; const optionalMigrations = []; for (const name of collection.listSchematicNames()) { const schematic = workflow.engine.createSchematic(name, collection); const description = schematic.description; description.version = coerceVersionNumber(description.version); if (!description.version) { continue; } if (semver.satisfies(description.version, migrationRange, { includePrerelease: true })) { (description.optional ? optionalMigrations : requiredMigrations).push(description); } } if (requiredMigrations.length === 0 && optionalMigrations.length === 0) { return 0; } // Required migrations if (requiredMigrations.length) { this.context.logger.info(color_1.colors.cyan(`** Executing migrations of package '${packageName}' **\n`)); requiredMigrations.sort((a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name)); const result = await this.executePackageMigrations(workflow, requiredMigrations, packageName, commit); if (result === 1) { return 1; } } // Optional migrations if (optionalMigrations.length) { this.context.logger.info(color_1.colors.magenta(`** Optional migrations of package '${packageName}' **\n`)); optionalMigrations.sort((a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name)); const migrationsToRun = await this.getOptionalMigrationsToRun(optionalMigrations, packageName); if (migrationsToRun?.length) { return this.executePackageMigrations(workflow, migrationsToRun, packageName, commit); } } return 0; } async executePackageMigrations(workflow, migrations, packageName, commit = false) { const { logger } = this.context; for (const migration of migrations) { const { title, description } = getMigrationTitleAndDescription(migration); logger.info(color_1.colors.cyan(color_1.figures.pointer) + ' ' + color_1.colors.bold(title)); if (description) { logger.info(' ' + description); } const { success, files } = await this.executeSchematic(workflow, migration.collection.name, migration.name); if (!success) { return 1; } let modifiedFilesText; switch (files.size) { case 0: modifiedFilesText = 'No changes made'; break; case 1: modifiedFilesText = '1 file modified'; break; default: modifiedFilesText = `${files.size} files modified`; break; } logger.info(` Migration completed (${modifiedFilesText}).`); // Commit migration if (commit) { const commitPrefix = `${packageName} migration - ${migration.name}`; const commitMessage = migration.description ? `${commitPrefix}\n\n${migration.description}` : commitPrefix; const committed = this.commit(commitMessage); if (!committed) { // Failed to commit, something went wrong. Abort the update. return 1; } } logger.info(''); // Extra trailing newline. } return 0; } async migrateOnly(workflow, packageName, rootDependencies, options) { const { logger } = this.context; const packageDependency = rootDependencies.get(packageName); let packagePath = packageDependency?.path; let packageNode = packageDependency?.package; if (packageDependency && !packageNode) { 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 = (0, package_tree_1.findPackageJson)(this.context.root, packageName); if (packageJson) { packagePath = path.dirname(packageJson); packageNode = await (0, package_tree_1.readPackageJson)(packageJson); } } if (!packageNode || !packagePath) { logger.error('Package is not installed.'); return 1; } const updateMetadata = packageNode['ng-update']; let migrations = updateMetadata?.migrations; if (migrations === undefined) { logger.error('Package does not provide migrations.'); return 1; } else if (typeof migrations !== 'string') { logger.error('Package contains a malformed migrations field.'); return 1; } else if (path.posix.isAbsolute(migrations) || path.win32.isAbsolute(migrations)) { logger.error('Package contains an invalid migrations field. Absolute paths are not permitted.'); return 1; } // Normalize slashes migrations = migrations.replace(/\\/g, '/'); if (migrations.startsWith('../')) { 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 ((0, node_fs_1.existsSync)(localMigrations)) { migrations = localMigrations; } else { // Try to resolve from package location. // This avoids issues with package hoisting. try { const packageRequire = (0, node_module_1.createRequire)(packagePath + '/'); migrations = packageRequire.resolve(migrations, { paths: this.resolvePaths }); } catch (e) { (0, error_1.assertIsError)(e); if (e.code === 'MODULE_NOT_FOUND') { logger.error('Migrations for package were not found.'); } else { logger.error(`Unable to resolve migrations for package. [${e.message}]`); } return 1; } } if (options.name) { return this.executeMigration(workflow, packageName, migrations, options.name, options.createCommits); } const from = coerceVersionNumber(options.from); if (!from) { logger.error(`"from" value [${options.from}] is not a valid version.`); return 1; } return this.executeMigrations(workflow, packageName, migrations, from, options.to || packageNode.version, options.createCommits); } // eslint-disable-next-line max-lines-per-function async updatePackagesAndMigrate(workflow, rootDependencies, options, packages) { const { logger } = this.context; const logVerbose = (message) => { if (options.verbose) { logger.info(message); } }; const requests = []; // Validate packages actually are part of the workspace for (const pkg of packages) { const node = rootDependencies.get(pkg.name); if (!node?.package) { 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) { logger.info(`Package '${pkg.name}' is already at '${pkg.fetchSpec}'.`); continue; } requests.push({ identifier: pkg, node }); } if (requests.length === 0) { return 0; } logger.info('Fetching dependency metadata from registry...'); const packagesToUpdate = []; 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 (0, package_metadata_1.fetchPackageMetadata)(packageName, logger, { verbose: options.verbose, }); } catch (e) { (0, error_1.assertIsError)(e); 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; if (requestIdentifier.type === 'version' || requestIdentifier.type === 'range' || requestIdentifier.type === 'tag') { try { manifest = (0, npm_pick_manifest_1.default)(metadata, requestIdentifier.fetchSpec); } catch (e) { (0, error_1.assertIsError)(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 = (0, npm_pick_manifest_1.default)(metadata, 'latest'); } catch (e) { (0, error_1.assertIsError)(e); if (e.code !== 'ETARGET' && e.code !== 'ENOVERSIONS') { throw e; } } } } else if (e.code !== 'ENOVERSIONS') { throw e; } } } if (!manifest) { logger.error(`Package specified by '${requestIdentifier.raw}' does not exist within the registry.`); return 1; } if (manifest.version === node.package?.version) { 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. 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.dev/.`); } else { const nextMajorVersionFromCurrent = currentMajorVersion + 1; 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.dev/?v=${currentMajorVersion}.0-${nextMajorVersionFromCurrent}.0`); } return 1; } } packagesToUpdate.push(requestIdentifier.toString()); } if (packagesToUpdate.length === 0) { return 0; } const { success } = await this.executeSchematic(workflow, UPDATE_SCHEMATIC_COLLECTION, 'update', { verbose: options.verbose, force: options.force, next: options.next, packageManager: this.context.packageManager.name, packages: packagesToUpdate, }); if (success) { const { root: commandRoot, packageManager } = this.context; const installArgs = this.packageManagerForce(options.verbose) ? ['--force'] : []; const tasks = new listr2_1.Listr([ { title: 'Cleaning node modules directory', async task(_, task) { try { await node_fs_1.promises.rm(path.join(commandRoot, 'node_modules'), { force: true, recursive: true, maxRetries: 3, }); } catch (e) { (0, error_1.assertIsError)(e); if (e.code === 'ENOENT') { task.skip('Cleaning not required. Node modules directory not found.'); } } }, }, { title: 'Installing packages', async task() { const installationSuccess = await packageManager.installAll(installArgs, commandRoot); if (!installationSuccess) { throw new CommandError('Unable to install packages'); } }, }, ]); try { await tasks.run(); } catch (e) { if (e instanceof CommandError) { return 1; } throw e; } } if (success && options.createCommits) { if (!this.commit(`Angular CLI update for packages - ${packagesToUpdate.join(', ')}`)) { 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.externalMigrations; if (success && migrations) { const rootRequire = (0, node_module_1.createRequire)(this.context.root + '/'); 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 rootRequire.resolve(path.join(migration.package, 'package.json'))); } catch (e) { (0, error_1.assertIsError)(e); if (e.code === 'MODULE_NOT_FOUND') { // Fallback to trying to resolve the package's main entry point packagePath = rootRequire.resolve(migration.package); } else { throw e; } } } catch (e) { (0, error_1.assertIsError)(e); if (e.code === 'MODULE_NOT_FOUND') { logVerbose(e.toString()); logger.error(`Migrations for package (${migration.package}) were not found.` + ' The package could not be found in the workspace.'); } else { 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 ((0, node_fs_1.existsSync)(localMigrations)) { migrations = localMigrations; } else { // Try to resolve from package location. // This avoids issues with package hoisting. try { const packageRequire = (0, node_module_1.createRequire)(packagePath + '/'); migrations = packageRequire.resolve(migration.collection); } catch (e) { (0, error_1.assertIsError)(e); if (e.code === 'MODULE_NOT_FOUND') { logger.error(`Migrations for package (${migration.package}) were not found.`); } else { logger.error(`Unable to resolve migrations for package (${migration.package}). [${e.message}]`); } return 1; } } const result = await this.executeMigrations(workflow, migration.package, migrations, migration.from, migration.to, options.createCommits); // A non-zero value is a failure for the package's migrations if (result !== 0) { return result; } } } return success ? 0 : 1; } /** * @return Whether or not the commit was successful. */ commit(message) { const { logger } = this.context; // Check if a commit is needed. let commitNeeded; try { commitNeeded = hasChangesToCommit(); } catch (err) { logger.error(` Failed to read Git tree:\n${err.stderr}`); return false; } if (!commitNeeded) { logger.info(' No changes to commit after migration.'); return true; } // Commit changes and abort on error. try { createCommit(message); } catch (err) { logger.error(`Failed to commit update (${message}):\n${err.stderr}`); return false; } // Notify user of the commit. const hash = findCurrentGitSha(); const shortMessage = message.split('\n')[0]; if (hash) { logger.info(` Committed migration step (${getShortHash(hash)}): ${shortMessage}.`); } else { // Commit was successful, but reading the hash was not. Something weird happened, // but nothing that would stop the update. Just log the weirdness and continue. logger.info(` Committed migration step: ${shortMessage}.`); logger.warn(' Failed to look up hash of most recent commit, continuing anyways.'); } return true; } checkCleanGit() { try { const topLevel = (0, node_child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: 'pipe', }); const result = (0, node_child_process_1.execSync)('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' }); if (result.trim().length === 0) { return true; } // Only files inside the workspace root are relevant for (const entry of result.split('\n')) { const relativeEntry = path.relative(path.resolve(this.context.root), path.resolve(topLevel.trim(), entry.slice(3).trim())); if (!relativeEntry.startsWith('..') && !path.isAbsolute(relativeEntry)) { return false; } } } catch { } return true; } /** * Checks if the current installed CLI version is older or newer than a compatible version. * @returns the version to install or null when there is no update to install. */ async checkCLIVersion(packagesToUpdate, verbose = false, next = false) { const { version } = await (0, package_metadata_1.fetchPackageManifest)(`@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`, this.context.logger, { verbose, usingYarn: this.context.packageManager.name === workspace_schema_1.PackageManager.Yarn, }); return version_1.VERSION.full === version ? null : version; } getCLIUpdateRunnerVersion(packagesToUpdate, next) { if (next) { return 'next'; } const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r)); if (updatingAngularPackage) { // If we are updating any Angular package we can update the CLI to the target version because // migrations for @angular/core@13 can be executed using Angular/cli@13. // This is same behaviour as `npx @angular/cli@13 update @angular/core@13`. // `@angular/cli@13` -> ['', 'angular/cli', '13'] // `@angular/cli` -> ['', 'angular/cli'] const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]); return semver.parse(tempVersion)?.major ?? 'latest'; } // When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in. // Typically, we can assume that the `@angular/cli` was updated previously. // Example: Angular official packages are typically updated prior to NGRX etc... // Therefore, we only update to the latest patch version of the installed major version of the Angular CLI. // This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12. // We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic. return version_1.VERSION.major; } async runTempBinary(packageName, args = []) { const { success, tempNodeModules } = await this.context.packageManager.installTemp(packageName); if (!success) { return 1; } // Remove version/tag etc... from package name // Ex: @angular/cli@latest -> @angular/cli const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@')); const pkgLocation = (0, node_path_1.join)(tempNodeModules, packageNameNoVersion); const packageJsonPath = (0, node_path_1.join)(pkgLocation, 'package.json'); // Get a binary location for this package let binPath; if ((0, node_fs_1.existsSync)(packageJsonPath)) { const content = await node_fs_1.promises.readFile(packageJsonPath, 'utf-8'); if (content) { const { bin = {} } = JSON.parse(content); const binKeys = Object.keys(bin); if (binKeys.length) { binPath = (0, node_path_1.resolve)(pkgLocation, bin[binKeys[0]]); } } } if (!binPath) { throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`); } const { status, error } = (0, node_child_process_1.spawnSync)(process.execPath, [binPath, ...args], { stdio: 'inherit', env: { ...process.env, NG_DISABLE_VERSION_CHECK: 'true', NG_CLI_ANALYTICS: 'false', }, }); if (status === null && error) { throw error; } return status ?? 0; } packageManagerForce(verbose) { // npm 7+ can fail due to it incorrectly resolving peer dependencies that have valid SemVer // ranges during an update. Update will set correct versions of dependencies within the // package.json file. The force option is set to workaround these errors. // Example error: // npm ERR! Conflicting peer dependency: @angular/compiler-cli@14.0.0-rc.0 // npm ERR! node_modules/@angular/compiler-cli // npm ERR! peer @angular/compiler-cli@"^14.0.0 || ^14.0.0-rc" from @angular-devkit/build-angular@14.0.0-rc.0 // npm ERR! node_modules/@angular-devkit/build-angular // npm ERR! dev @angular-devkit/build-angular@"~14.0.0-rc.0" from the root project if (this.context.packageManager.name === workspace_schema_1.PackageManager.Npm && this.context.packageManager.version && semver.gte(this.context.packageManager.version, '7.0.0')) { if (verbose) { this.context.logger.info('NPM 7+ detected -- enabling force option for package installation'); } return true; } return false; } async getOptionalMigrationsToRun(optionalMigrations, packageName) { const { logger } = this.context; const numberOfMigrations = optionalMigrations.length; logger.info(`This package has ${numberOfMigrations} optional migration${numberOfMigrations > 1 ? 's' : ''} that can be executed.`); if (!(0, tty_1.isTTY)()) { for (const migration of optionalMigrations) { const { title } = getMigrationTitleAndDescription(migration); logger.info(color_1.colors.cyan(color_1.figures.pointer) + ' ' + color_1.colors.bold(title)); logger.info(color_1.colors.gray(` ng update ${packageName} --name ${migration.name}`)); logger.info(''); // Extra trailing newline. } return undefined; } logger.info('Optional migrations may be skipped and executed after the update process, if preferred.'); logger.info(''); // Extra trailing newline. const answer = await (0, prompt_1.askChoices)(`Select the migrations that you'd like to run`, optionalMigrations.map((migration) => { const { title, documentation } = getMigrationTitleAndDescription(migration); return { name: `[${color_1.colors.white(migration.name)}] ${title}${documentation ? ` (${documentation})` : ''}`, value: migration.name, checked: migration.recommended, }; }), null); logger.info(''); // Extra trailing newline. return optionalMigrations.filter(({ name }) => answer?.includes(name)); } } exports.default = UpdateCommandModule; /** * @return Whether or not the working directory has Git changes to commit. */ function hasChangesToCommit() { // List all modified files not covered by .gitignore. // If any files are returned, then there must be something to commit. return (0, node_child_process_1.execSync)('git ls-files -m -d -o --exclude-standard').toString() !== ''; } /** * Precondition: Must have pending changes to commit, they do not need to be staged. * Postcondition: The Git working tree is committed and the repo is clean. * @param message The commit message to use. */ function createCommit(message) { // Stage entire working tree for commit. (0, node_child_process_1.execSync)('git add -A', { encoding: 'utf8', stdio: 'pipe' }); // Commit with the message passed via stdin to avoid bash escaping issues. (0, node_child_process_1.execSync)('git commit --no-verify -F -', { encoding: 'utf8', stdio: 'pipe', input: message }); } /** * @return The Git SHA hash of the HEAD commit. Returns null if unable to retrieve the hash. */ function findCurrentGitSha() { try { return (0, node_child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' }).trim(); } catch { return null; } } function getShortHash(commitHash) { return commitHash.slice(0, 9); } function coerceVersionNumber(version) { if (!version) { return undefined; } if (!/^\d{1,30}\.\d{1,30}\.\d{1,30}/.test(version)) { const match = version.match(/^\d{1,30}(\.\d{1,30})*/); if (!match) { return undefined; } if (!match[1]) { version = version.substring(0, match[0].length) + '.0.0' + version.substring(match[0].length); } else if (!match[2]) { version = version.substring(0, match[0].length) + '.0' + version.substring(match[0].length); } else { return undefined; } } return semver.valid(version) ?? undefined; } function getMigrationTitleAndDescription(migration) { const [title, ...description] = migration.description.split('. '); return { title: title.endsWith('.') ? title : title + '.', description: description.join('.\n '), documentation: migration.documentation ? new URL(migration.documentation, 'https://angular.dev').href : undefined, }; }