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
14 changes: 7 additions & 7 deletions packages/angular/cli/src/command-builder/command-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { logging, schema } from '@angular-devkit/core';
import { schema } from '@angular-devkit/core';
import { readFileSync } from 'node:fs';
import * as path from 'node:path';
import { join, posix, relative } from 'node:path';
import type { ArgumentsCamelCase, Argv, CommandModule as YargsCommandModule } from 'yargs';
import { Parser as yargsParser } from 'yargs/helpers';
import { getAnalyticsUserId } from '../analytics/analytics';
Expand All @@ -17,7 +17,6 @@ import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-
import { considerSettingUpAutocompletion } from '../utilities/completion';
import { AngularWorkspace } from '../utilities/config';
import { memoize } from '../utilities/memoize';
import { PackageManagerUtils } from '../utilities/package-manager';
import { CommandContext, CommandScope, Options, OtherOptions } from './definitions';
import { Option, addSchemaOptionsToCommand } from './utilities/json-schema';

Expand Down Expand Up @@ -74,9 +73,10 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
describe: this.describe,
...(this.longDescriptionPath
? {
longDescriptionRelativePath: path
.relative(path.join(__dirname, '../../../../'), this.longDescriptionPath)
.replace(/\\/g, path.posix.sep),
longDescriptionRelativePath: relative(
join(__dirname, '../../../../'),
this.longDescriptionPath,
).replace(/\\/g, posix.sep),
longDescription: readFileSync(this.longDescriptionPath, 'utf8').replace(
/\r\n/g,
'\n',
Expand Down Expand Up @@ -156,7 +156,7 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
return userId
? new AnalyticsCollector(this.context.logger, userId, {
name: this.context.packageManager.name,
version: this.context.packageManager.version,
version: await this.context.packageManager.getVersion(),
})
: undefined;
}
Expand Down
86 changes: 80 additions & 6 deletions packages/angular/cli/src/command-builder/command-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@
* 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 { getCacheConfig } from '../commands/cache/utilities';
import {
CommandConfig,
CommandNames,
RootCommands,
RootCommandsAliases,
} from '../commands/command-config';
import { createPackageManager } from '../package-managers';
import { ConfiguredPackageManagerInfo } from '../package-managers/factory';
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 +38,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 @@ -60,16 +65,28 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
}

const root = workspace?.basePath ?? process.cwd();
const localYargs = yargs(args);
const cacheConfig = workspace && getCacheConfig(workspace);
const packageManager = await createPackageManager({
cwd: root,
logger,
dryRun: dryRun || help || jsonHelp || getYargsCompletions,
tempDirectory: cacheConfig?.enabled ? cacheConfig.path : undefined,
configuredPackageManager: await getConfiguredPackageManager(
root,
workspace,
globalConfiguration,
),
});

const localYargs = yargs(args);
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 +180,60 @@ 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 and version.
*/
async function getConfiguredPackageManager(
root: string,
localWorkspace: AngularWorkspace | undefined,
globalWorkspace: AngularWorkspace,
): Promise<ConfiguredPackageManagerInfo | undefined> {
let result: ConfiguredPackageManagerInfo | 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 and version.
*/
function getPackageManager(
source: JsonValue | undefined,
): ConfiguredPackageManagerInfo | undefined {
if (source && isJsonObject(source)) {
const value = source['packageManager'];
if (typeof value === 'string') {
return value.split('@', 2) as unknown as ConfiguredPackageManagerInfo;
}
}

return undefined;
}
4 changes: 2 additions & 2 deletions packages/angular/cli/src/command-builder/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import { logging } from '@angular-devkit/core';
import type { Argv, CamelCaseKey } from 'yargs';
import type { PackageManager } from '../package-managers/package-manager';
import { AngularWorkspace } from '../utilities/config';
import { PackageManagerUtils } from '../utilities/package-manager';

export enum CommandScope {
/** Command can only run inside an Angular workspace. */
Expand All @@ -28,7 +28,7 @@ export interface CommandContext {
workspace?: AngularWorkspace;
globalConfiguration: AngularWorkspace;
logger: logging.Logger;
packageManager: PackageManagerUtils;
packageManager: PackageManager;
yargsInstance: Argv<{}>;

/** Arguments parsed in free-from without parser configuration. */
Expand Down
61 changes: 15 additions & 46 deletions packages/angular/cli/src/commands/add/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Listr, ListrRenderer, ListrTaskWrapper, color, figures } from 'listr2';
import assert from 'node:assert';
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, join, relative, resolve } from 'node:path';
import { dirname, join } from 'node:path';
import npa from 'npm-package-arg';
import semver, { Range, compare, intersects, prerelease, satisfies, valid } from 'semver';
import { Argv } from 'yargs';
Expand All @@ -25,16 +25,13 @@ 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';
import { VERSION } from '../../utilities/version';
import { getCacheConfig } from '../cache/utilities';

class CommandError extends Error {}

Expand All @@ -46,7 +43,6 @@ interface AddCommandArgs extends SchematicsCommandArgs {
}

interface AddCommandTaskContext {
packageManager: PackageManager;
packageIdentifier: npa.Result;
savePackage?: NgAddSaveDependency;
collectionName?: string;
Expand Down Expand Up @@ -198,7 +194,8 @@ export default class AddCommandModule
[
{
title: 'Determining Package Manager',
task: (context, task) => this.determinePackageManagerTask(context, task),
task: (_context, task) =>
(task.output = `Using package manager: ${color.dim(this.context.packageManager.name)}`),
rendererOptions: { persistentOutput: true },
},
{
Expand Down Expand Up @@ -309,47 +306,14 @@ export default class AddCommandModule
}
}

private async determinePackageManagerTask(
context: AddCommandTaskContext,
task: AddCommandTaskWrapper,
): Promise<void> {
let tempDirectory: string | undefined;
const tempOptions = ['node_modules'];

const cacheConfig = getCacheConfig(this.context.workspace);
if (cacheConfig.enabled) {
const cachePath = resolve(this.context.root, cacheConfig.path);
if (!relative(this.context.root, cachePath).startsWith('..')) {
tempOptions.push(cachePath);
}
}

for (const tempOption of tempOptions) {
try {
const directory = resolve(this.context.root, tempOption);
if ((await fs.stat(directory)).isDirectory()) {
tempDirectory = directory;
break;
}
} 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)}`;
}

private async findCompatiblePackageVersionTask(
context: AddCommandTaskContext,
task: AddCommandTaskWrapper,
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 +410,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 +489,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 +553,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
15 changes: 2 additions & 13 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 type { 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 @@ -168,13 +163,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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process';
import { existsSync, promises as fs } from 'node:fs';
import { join, resolve } from 'node:path';
import * as semver from 'semver';
import { PackageManager } from '../../../package-managers';
import type { PackageManager } from '../../../package-managers';
import { VERSION } from '../../../utilities/version';
import { ANGULAR_PACKAGES_REGEXP } from './constants';

Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/version/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default class VersionCommandModule
*/
async run(options: { json?: boolean }): Promise<void> {
const { logger } = this.context;
const versionInfo = gatherVersionInfo(this.context);
const versionInfo = await gatherVersionInfo(this.context);

if (options.json) {
// eslint-disable-next-line no-console
Expand Down
Loading