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
24 changes: 13 additions & 11 deletions packages/angular/cli/src/command-builder/command-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import { Parser as yargsParser } from 'yargs/helpers';
import { getAnalyticsUserId } from '../analytics/analytics';
import { AnalyticsCollector } from '../analytics/analytics-collector';
import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-parameters';
import { PackageManager } from '../package-managers';
import { considerSettingUpAutocompletion } from '../utilities/completion';
import { AngularWorkspace } from '../utilities/config';
import { memoize } from '../utilities/memoize';
import { PackageManagerUtils } from '../utilities/package-manager';
import { Option, addSchemaOptionsToCommand } from './utilities/json-schema';

export type Options<T> = { [key in keyof T as CamelCaseKey<key>]: T[key] };
Expand All @@ -39,16 +39,16 @@ export enum CommandScope {
}

export interface CommandContext {
currentDirectory: string;
root: string;
workspace?: AngularWorkspace;
globalConfiguration: AngularWorkspace;
logger: logging.Logger;
packageManager: PackageManagerUtils;
yargsInstance: Argv<{}>;
readonly currentDirectory: string;
readonly root: string;
readonly workspace?: AngularWorkspace;
readonly globalConfiguration: AngularWorkspace;
readonly logger: logging.Logger;
readonly packageManager: PackageManager;
readonly yargsInstance: Argv<{}>;

/** Arguments parsed in free-from without parser configuration. */
args: {
readonly args: {
positional: string[];
options: {
help: boolean;
Expand All @@ -60,8 +60,10 @@ export interface CommandContext {

export type OtherOptions = Record<string, unknown>;

export interface CommandModuleImplementation<T extends {} = {}>
extends Omit<YargsCommandModule<{}, T>, 'builder' | 'handler'> {
export interface CommandModuleImplementation<T extends {} = {}> extends Omit<
YargsCommandModule<{}, T>,
'builder' | 'handler'
> {
/** Scope in which the command can be executed in. */
scope: CommandScope;

Expand Down
79 changes: 74 additions & 5 deletions packages/angular/cli/src/command-builder/command-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { logging } from '@angular-devkit/core';
import { JsonValue, isJsonObject, logging } from '@angular-devkit/core';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import yargs from 'yargs';
import { Parser as yargsParser } from 'yargs/helpers';
import {
Expand All @@ -15,10 +17,10 @@ import {
RootCommands,
RootCommandsAliases,
} from '../commands/command-config';
import { PackageManagerName, createPackageManager } from '../package-managers';
import { colors } from '../utilities/color';
import { AngularWorkspace, getWorkspace } from '../utilities/config';
import { AngularWorkspace, getProjectByCwd, getWorkspace } from '../utilities/config';
import { assertIsError } from '../utilities/error';
import { PackageManagerUtils } from '../utilities/package-manager';
import { VERSION } from '../utilities/version';
import { CommandContext, CommandModuleError } from './command-module';
import {
Expand All @@ -34,11 +36,12 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
$0,
_,
help = false,
dryRun = false,
jsonHelp = false,
getYargsCompletions = false,
...rest
} = yargsParser(args, {
boolean: ['help', 'json-help', 'get-yargs-completions'],
boolean: ['help', 'json-help', 'get-yargs-completions', 'dry-run'],
alias: { 'collection': 'c' },
});

Expand All @@ -62,14 +65,25 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
const root = workspace?.basePath ?? process.cwd();
const localYargs = yargs(args);

const packageManager = await createPackageManager({
cwd: root,
logger,
dryRun,
configuredPackageManager: await getConfiguredPackageManager(
root,
workspace,
globalConfiguration,
),
});

const context: CommandContext = {
globalConfiguration,
workspace,
logger,
currentDirectory: process.cwd(),
yargsInstance: localYargs,
root,
packageManager: new PackageManagerUtils({ globalConfiguration, workspace, root }),
packageManager,
args: {
positional: positional.map((v) => v.toString()),
options: {
Expand Down Expand Up @@ -163,3 +177,58 @@ async function getCommandsToRegister(

return Promise.all(commands.map((command) => command.factory().then((m) => m.default)));
}

/**
* Gets the configured package manager by checking package.json, or the local and global angular.json files.
*
* @param root The root directory of the workspace.
* @param localWorkspace The local workspace.
* @param globalWorkspace The global workspace.
* @returns The package manager name.
*/
async function getConfiguredPackageManager(
root: string,
localWorkspace: AngularWorkspace | undefined,
globalWorkspace: AngularWorkspace,
): Promise<PackageManagerName | undefined> {
let result: PackageManagerName | undefined;

try {
const packageJsonPath = join(root, 'package.json');
const pkgJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as JsonValue;
result = getPackageManager(pkgJson);
} catch {}

if (result) {
return result;
}

if (localWorkspace) {
const project = getProjectByCwd(localWorkspace);
if (project) {
result = getPackageManager(localWorkspace.projects.get(project)?.extensions['cli']);
}

result ??= getPackageManager(localWorkspace.extensions['cli']);
}

result ??= getPackageManager(globalWorkspace.extensions['cli']);

return result;
}

/**
* Get the package manager name from a JSON value.
* @param source The JSON value to get the package manager name from.
* @returns The package manager name.
*/
function getPackageManager(source: JsonValue | undefined): PackageManagerName | undefined {
if (source && isJsonObject(source)) {
const value = source['packageManager'];
if (typeof value === 'string') {
return value.split('@', 1)[0] as unknown as PackageManagerName;
}
}

return undefined;
}
31 changes: 15 additions & 16 deletions packages/angular/cli/src/commands/add/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ import {
} from '../../command-builder/schematics-command-module';
import {
NgAddSaveDependency,
PackageManager,
PackageManagerError,
PackageManifest,
PackageMetadata,
createPackageManager,
} from '../../package-managers';
import { assertIsError } from '../../utilities/error';
import { isTTY } from '../../utilities/tty';
Expand All @@ -46,7 +44,6 @@ interface AddCommandArgs extends SchematicsCommandArgs {
}

interface AddCommandTaskContext {
packageManager: PackageManager;
packageIdentifier: npa.Result;
savePackage?: NgAddSaveDependency;
collectionName?: string;
Expand Down Expand Up @@ -334,13 +331,9 @@ export default class AddCommandModule
} catch {}
}

context.packageManager = await createPackageManager({
cwd: this.context.root,
logger: this.context.logger,
dryRun: context.dryRun,
tempDirectory,
});
task.output = `Using package manager: ${color.dim(context.packageManager.name)}`;
this.context.packageManager.temporaryDirectory = tempDirectory;

task.output = `Using package manager: ${color.dim(this.context.packageManager.name)}`;
}

private async findCompatiblePackageVersionTask(
Expand All @@ -349,7 +342,8 @@ export default class AddCommandModule
options: Options<AddCommandArgs>,
): Promise<void> {
const { registry, verbose } = options;
const { packageManager, packageIdentifier } = context;
const { packageIdentifier } = context;
const { packageManager } = this.context;
const packageName = packageIdentifier.name;

assert(packageName, 'Registry package identifiers should always have a name.');
Expand Down Expand Up @@ -446,7 +440,8 @@ export default class AddCommandModule
rejectionReasons: string[];
},
): Promise<PackageManifest | null> {
const { packageManager, packageIdentifier } = context;
const { packageIdentifier } = context;
const { packageManager } = this.context;
const { registry, verbose, rejectionReasons } = options;
const packageName = packageIdentifier.name;
assert(packageName, 'Package name must be defined.');
Expand Down Expand Up @@ -524,9 +519,12 @@ export default class AddCommandModule

let manifest;
try {
manifest = await context.packageManager.getManifest(context.packageIdentifier.toString(), {
registry,
});
manifest = await this.context.packageManager.getManifest(
context.packageIdentifier.toString(),
{
registry,
},
);
} catch (e) {
assertIsError(e);
throw new CommandError(
Expand Down Expand Up @@ -585,7 +583,8 @@ export default class AddCommandModule
options: Options<AddCommandArgs>,
): Promise<void> {
const { registry } = options;
const { packageManager, packageIdentifier, savePackage } = context;
const { packageIdentifier, savePackage } = context;
const { packageManager } = this.context;

// Only show if installation will actually occur
task.title = 'Installing package';
Expand Down
19 changes: 4 additions & 15 deletions packages/angular/cli/src/commands/update/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,7 @@ import {
Options,
} from '../../command-builder/command-module';
import { SchematicEngineHost } from '../../command-builder/utilities/schematic-engine-host';
import {
InstalledPackage,
PackageManager,
PackageManifest,
createPackageManager,
} from '../../package-managers';
import { InstalledPackage, PackageManager, PackageManifest } from '../../package-managers';
import { colors } from '../../utilities/color';
import { disableVersionCheck } from '../../utilities/environment-options';
import { assertIsError } from '../../utilities/error';
Expand Down Expand Up @@ -169,13 +164,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
}

async run(options: Options<UpdateCommandArgs>): Promise<number | void> {
const { logger } = this.context;
// Instantiate the package manager
const packageManager = await createPackageManager({
cwd: this.context.root,
logger,
configuredPackageManager: this.context.packageManager.name,
});
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.
Expand Down Expand Up @@ -247,7 +236,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs

const workflow = new NodeWorkflow(this.context.root, {
packageManager: packageManager.name,
packageManagerForce: await shouldForcePackageManager(packageManager, logger, options.verbose),
packageManagerForce: shouldForcePackageManager(packageManager, logger, options.verbose),
// __dirname -> favor @schematics/update from this package
// Otherwise, use packages from the active workspace (migrations)
resolvePaths: this.resolvePaths,
Expand Down Expand Up @@ -537,7 +526,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs

if (success) {
const { root: commandRoot } = this.context;
const ignorePeerDependencies = await shouldForcePackageManager(
const ignorePeerDependencies = shouldForcePackageManager(
packageManager,
logger,
options.verbose,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,16 @@ export async function runTempBinary(
* @param verbose Whether to log verbose output.
* @returns True if the package manager should be forced, false otherwise.
*/
export async function shouldForcePackageManager(
export function shouldForcePackageManager(
packageManager: PackageManager,
logger: logging.LoggerApi,
verbose: boolean,
): Promise<boolean> {
): boolean {
// 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.
if (packageManager.name === 'npm') {
const version = await packageManager.getVersion();
const version = packageManager.version;
if (semver.gte(version, '7.0.0')) {
if (verbose) {
logger.info('NPM 7+ detected -- enabling force option for package installation');
Expand Down
7 changes: 3 additions & 4 deletions packages/angular/cli/src/package-managers/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import assert from 'node:assert';
import { major } from 'semver';
import { discover } from './discovery';
import { Host, NodeJS_HOST } from './host';
Expand Down Expand Up @@ -110,9 +111,8 @@ export async function createPackageManager(options: {
configuredPackageManager?: PackageManagerName;
logger?: Logger;
dryRun?: boolean;
tempDirectory?: string;
}): Promise<PackageManager> {
const { cwd, configuredPackageManager, logger, dryRun, tempDirectory } = options;
const { cwd, configuredPackageManager, logger, dryRun } = options;
const host = NodeJS_HOST;

const { name, source } = await determinePackageManager(
Expand All @@ -131,13 +131,12 @@ export async function createPackageManager(options: {
const packageManager = new PackageManager(host, cwd, descriptor, {
dryRun,
logger,
tempDirectory,
});

// Do not verify if the package manager is installed during a dry run.
if (!dryRun) {
try {
await packageManager.getVersion();
assert(packageManager.version);
} catch {
if (source === 'default') {
throw new Error(
Expand Down
Loading