diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index d036656cf2dd..7e3925f5a52c 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -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 = { [key in keyof T as CamelCaseKey]: T[key] }; @@ -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; @@ -60,8 +60,10 @@ export interface CommandContext { export type OtherOptions = Record; -export interface CommandModuleImplementation - extends Omit, 'builder' | 'handler'> { +export interface CommandModuleImplementation extends Omit< + YargsCommandModule<{}, T>, + 'builder' | 'handler' +> { /** Scope in which the command can be executed in. */ scope: CommandScope; diff --git a/packages/angular/cli/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts index cb4ab2c8467e..a81570026d30 100644 --- a/packages/angular/cli/src/command-builder/command-runner.ts +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -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 { @@ -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 { @@ -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' }, }); @@ -62,6 +65,17 @@ 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, @@ -69,7 +83,7 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis currentDirectory: process.cwd(), yargsInstance: localYargs, root, - packageManager: new PackageManagerUtils({ globalConfiguration, workspace, root }), + packageManager, args: { positional: positional.map((v) => v.toString()), options: { @@ -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 { + 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; +} diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index 0531604d63d1..74bbeab41107 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -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'; @@ -46,7 +44,6 @@ interface AddCommandArgs extends SchematicsCommandArgs { } interface AddCommandTaskContext { - packageManager: PackageManager; packageIdentifier: npa.Result; savePackage?: NgAddSaveDependency; collectionName?: string; @@ -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( @@ -349,7 +342,8 @@ export default class AddCommandModule options: Options, ): Promise { 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.'); @@ -446,7 +440,8 @@ export default class AddCommandModule rejectionReasons: string[]; }, ): Promise { - 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.'); @@ -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( @@ -585,7 +583,8 @@ export default class AddCommandModule options: Options, ): Promise { 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'; diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts index b5f0be73539e..d79000e4988e 100644 --- a/packages/angular/cli/src/commands/update/cli.ts +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -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'; @@ -169,13 +164,7 @@ export default class UpdateCommandModule extends CommandModule): Promise { - 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. @@ -247,7 +236,7 @@ export default class UpdateCommandModule extends CommandModule favor @schematics/update from this package // Otherwise, use packages from the active workspace (migrations) resolvePaths: this.resolvePaths, @@ -537,7 +526,7 @@ export default class UpdateCommandModule extends CommandModule { +): 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'); diff --git a/packages/angular/cli/src/package-managers/factory.ts b/packages/angular/cli/src/package-managers/factory.ts index 1cd3d2462edc..3d8646a1babe 100644 --- a/packages/angular/cli/src/package-managers/factory.ts +++ b/packages/angular/cli/src/package-managers/factory.ts @@ -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'; @@ -110,9 +111,8 @@ export async function createPackageManager(options: { configuredPackageManager?: PackageManagerName; logger?: Logger; dryRun?: boolean; - tempDirectory?: string; }): Promise { - const { cwd, configuredPackageManager, logger, dryRun, tempDirectory } = options; + const { cwd, configuredPackageManager, logger, dryRun } = options; const host = NodeJS_HOST; const { name, source } = await determinePackageManager( @@ -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( diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 1faedc5b155e..d65979895f6c 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -12,6 +12,7 @@ * a flexible and secure abstraction over the various package managers. */ +import { execSync } from 'node:child_process'; import { join } from 'node:path'; import npa from 'npm-package-arg'; import { maxSatisfying } from 'semver'; @@ -80,6 +81,9 @@ export class PackageManager { readonly #metadataCache = new Map(); #dependencyCache: Map | null = null; + /** The path to use as the base for temporary directories. */ + temporaryDirectory?: string; + /** * Creates a new `PackageManager` instance. * @param host A `Host` instance for interacting with the file system and running commands. @@ -329,14 +333,34 @@ export class PackageManager { this.#dependencyCache = null; } + #version: string | undefined; + /** * Gets the version of the package manager binary. - * @returns A promise that resolves to the trimmed version string. */ - async getVersion(): Promise { - const { stdout } = await this.#run(this.descriptor.versionCommand); + get version(): string { + if (!this.#version) { + this.#version = this.getVersion(); + } + + return this.#version; + } - return stdout.trim(); + /** + * Gets the version of the package manager binary. + * @returns The version of the package manager binary. + */ + private getVersion(): string { + return execSync(`${this.descriptor.binary} ${this.descriptor.versionCommand}`, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + env: { + ...process.env, + // NPM updater notifier will prevents the child process from closing until it timeout after 3 minutes. + NO_UPDATE_NOTIFIER: '1', + NPM_CONFIG_UPDATE_NOTIFIER: 'false', + }, + }).trim(); } /** @@ -544,7 +568,7 @@ export class PackageManager { specifier: string, options: { registry?: string; ignoreScripts?: boolean } = {}, ): Promise<{ workingDirectory: string; cleanup: () => Promise }> { - const workingDirectory = await this.host.createTempDirectory(this.options.tempDirectory); + const workingDirectory = await this.host.createTempDirectory(this.temporaryDirectory); const cleanup = () => this.host.deleteDirectory(workingDirectory); // Some package managers, like yarn classic, do not write a package.json when adding a package. diff --git a/packages/angular/cli/src/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts deleted file mode 100644 index b913a3bfd72d..000000000000 --- a/packages/angular/cli/src/utilities/package-manager.ts +++ /dev/null @@ -1,339 +0,0 @@ -/** - * @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 - */ - -import { JsonValue, isJsonObject } from '@angular-devkit/core'; -import { execSync, spawn } from 'node:child_process'; -import { promises as fs, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { PackageManager } from '../../lib/config/workspace-schema'; -import { AngularWorkspace, getProjectByCwd } from './config'; -import { memoize } from './memoize'; - -/** - * A map of package managers to their corresponding lockfile names. - */ -const LOCKFILE_NAMES: Readonly> = { - [PackageManager.Yarn]: 'yarn.lock', - [PackageManager.Pnpm]: 'pnpm-lock.yaml', - [PackageManager.Bun]: ['bun.lockb', 'bun.lock'], - [PackageManager.Npm]: 'package-lock.json', -}; - -interface PackageManagerOptions { - saveDev: string; - install: string; - installAll?: string; - prefix: string; - noLockfile: string; -} - -export interface PackageManagerUtilsContext { - globalConfiguration: AngularWorkspace; - workspace?: AngularWorkspace; - root: string; -} - -/** - * Utilities for interacting with various package managers. - */ -export class PackageManagerUtils { - /** - * @param context The context for the package manager utilities, including workspace and global configuration. - */ - constructor(private readonly context: PackageManagerUtilsContext) {} - - /** Get the package manager name. */ - get name(): PackageManager { - return this.getName(); - } - - /** Get the package manager version. */ - get version(): string | undefined { - return this.getVersion(this.name); - } - - /** Install a single package. */ - async install( - packageName: string, - save: 'dependencies' | 'devDependencies' | boolean = true, - extraArgs: string[] = [], - cwd?: string, - ): Promise { - const packageManagerArgs = this.getArguments(); - const installArgs: string[] = [packageManagerArgs.install, packageName]; - - if (save === 'devDependencies') { - installArgs.push(packageManagerArgs.saveDev); - } else if (save === false) { - installArgs.push(packageManagerArgs.noLockfile); - } - - return this.run([...installArgs, ...extraArgs], { cwd, silent: true }); - } - - /** Install all packages. */ - async installAll(extraArgs: string[] = [], cwd?: string): Promise { - const packageManagerArgs = this.getArguments(); - const installArgs: string[] = []; - if (packageManagerArgs.installAll) { - installArgs.push(packageManagerArgs.installAll); - } - - return this.run([...installArgs, ...extraArgs], { cwd, silent: true }); - } - - /** Install a single package temporary. */ - async installTemp( - packageName: string, - extraArgs?: string[], - ): Promise<{ - success: boolean; - tempNodeModules: string; - }> { - const tempPath = await fs.mkdtemp(join(realpathSync(tmpdir()), 'angular-cli-packages-')); - - // clean up temp directory on process exit - process.on('exit', () => { - try { - rmSync(tempPath, { recursive: true, maxRetries: 3 }); - } catch {} - }); - - // NPM will warn when a `package.json` is not found in the install directory - // Example: - // npm WARN enoent ENOENT: no such file or directory, open '/tmp/.ng-temp-packages-84Qi7y/package.json' - // npm WARN .ng-temp-packages-84Qi7y No description - // npm WARN .ng-temp-packages-84Qi7y No repository field. - // npm WARN .ng-temp-packages-84Qi7y No license field. - - // While we can use `npm init -y` we will end up needing to update the 'package.json' anyways - // because of missing fields. - await fs.writeFile( - join(tempPath, 'package.json'), - JSON.stringify({ - name: 'temp-cli-install', - description: 'temp-cli-install', - repository: 'temp-cli-install', - license: 'MIT', - }), - ); - - // setup prefix/global modules path - const packageManagerArgs = this.getArguments(); - const tempNodeModules = join(tempPath, 'node_modules'); - // Yarn will not append 'node_modules' to the path - const prefixPath = this.name === PackageManager.Yarn ? tempNodeModules : tempPath; - const installArgs: string[] = [ - ...(extraArgs ?? []), - `${packageManagerArgs.prefix}="${prefixPath}"`, - packageManagerArgs.noLockfile, - ]; - - return { - success: await this.install(packageName, true, installArgs, tempPath), - tempNodeModules, - }; - } - - private getArguments(): PackageManagerOptions { - switch (this.name) { - case PackageManager.Yarn: - return { - saveDev: '--dev', - install: 'add', - prefix: '--modules-folder', - noLockfile: '--no-lockfile', - }; - case PackageManager.Pnpm: - return { - saveDev: '--save-dev', - install: 'add', - installAll: 'install', - prefix: '--prefix', - noLockfile: '--no-lockfile', - }; - case PackageManager.Bun: - return { - saveDev: '--dev', - install: 'add', - installAll: 'install', - prefix: '--cwd', - noLockfile: '--no-save', - }; - default: - return { - saveDev: '--save-dev', - install: 'install', - installAll: 'install', - prefix: '--prefix', - noLockfile: '--no-package-lock', - }; - } - } - - private async run( - args: string[], - options: { cwd?: string; silent?: boolean } = {}, - ): Promise { - const { cwd = process.cwd(), silent = false } = options; - - return new Promise((resolve) => { - const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer }[] = []; - - const childProcess = spawn(`${this.name} ${args.join(' ')}`, { - // Always pipe stderr to allow for failures to be reported - stdio: silent ? ['ignore', 'ignore', 'pipe'] : 'pipe', - shell: true, - cwd, - }).on('close', (code: number) => { - if (code === 0) { - resolve(true); - } else { - bufferedOutput.forEach(({ stream, data }) => stream.write(data)); - resolve(false); - } - }); - - childProcess.stdout?.on('data', (data: Buffer) => - bufferedOutput.push({ stream: process.stdout, data: data }), - ); - childProcess.stderr?.on('data', (data: Buffer) => - bufferedOutput.push({ stream: process.stderr, data: data }), - ); - }); - } - - @memoize - private getVersion(name: PackageManager): string | undefined { - try { - return execSync(`${name} --version`, { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - env: { - ...process.env, - // NPM updater notifier will prevents the child process from closing until it timeout after 3 minutes. - NO_UPDATE_NOTIFIER: '1', - NPM_CONFIG_UPDATE_NOTIFIER: 'false', - }, - }).trim(); - } catch { - return undefined; - } - } - - @memoize - private getName(): PackageManager { - const packageManager = this.getConfiguredPackageManager(); - if (packageManager) { - return packageManager; - } - - const filesInRoot = readdirSync(this.context.root); - const hasNpmLock = this.hasLockfile(PackageManager.Npm, filesInRoot); - const hasYarnLock = this.hasLockfile(PackageManager.Yarn, filesInRoot); - const hasPnpmLock = this.hasLockfile(PackageManager.Pnpm, filesInRoot); - const hasBunLock = this.hasLockfile(PackageManager.Bun, filesInRoot); - - // PERF NOTE: `this.getVersion` spawns the package a the child_process which can take around ~300ms at times. - // Therefore, we should only call this method when needed. IE: don't call `this.getVersion(PackageManager.Pnpm)` unless truly needed. - // The result of this method is not stored in a variable because it's memoized. - - if (hasNpmLock) { - // Has NPM lock file. - if (!hasYarnLock && !hasPnpmLock && !hasBunLock && this.getVersion(PackageManager.Npm)) { - // Only NPM lock file and NPM binary is available. - return PackageManager.Npm; - } - } else { - // No NPM lock file. - if (hasYarnLock && this.getVersion(PackageManager.Yarn)) { - // Yarn lock file and Yarn binary is available. - return PackageManager.Yarn; - } else if (hasPnpmLock && this.getVersion(PackageManager.Pnpm)) { - // PNPM lock file and PNPM binary is available. - return PackageManager.Pnpm; - } else if (hasBunLock && this.getVersion(PackageManager.Bun)) { - // Bun lock file and Bun binary is available. - return PackageManager.Bun; - } - } - - if (!this.getVersion(PackageManager.Npm)) { - // Doesn't have NPM installed. - const hasYarn = !!this.getVersion(PackageManager.Yarn); - const hasPnpm = !!this.getVersion(PackageManager.Pnpm); - const hasBun = !!this.getVersion(PackageManager.Bun); - - if (hasYarn && !hasPnpm && !hasBun) { - return PackageManager.Yarn; - } else if (hasPnpm && !hasYarn && !hasBun) { - return PackageManager.Pnpm; - } else if (hasBun && !hasYarn && !hasPnpm) { - return PackageManager.Bun; - } - } - - // TODO: This should eventually inform the user of ambiguous package manager usage. - // Potentially with a prompt to choose and optionally set as the default. - return PackageManager.Npm; - } - - /** - * Checks if a lockfile for a specific package manager exists in the root directory. - * @param packageManager The package manager to check for. - * @param filesInRoot An array of file names in the root directory. - * @returns True if the lockfile exists, false otherwise. - */ - private hasLockfile(packageManager: PackageManager, filesInRoot: string[]): boolean { - const lockfiles = LOCKFILE_NAMES[packageManager]; - - return typeof lockfiles === 'string' - ? filesInRoot.includes(lockfiles) - : lockfiles.some((lockfile) => filesInRoot.includes(lockfile)); - } - - private getConfiguredPackageManager(): PackageManager | undefined { - const { workspace: localWorkspace, globalConfiguration: globalWorkspace } = this.context; - let result: PackageManager | undefined; - - try { - const packageJsonPath = join(this.context.root, 'package.json'); - const pkgJson = JSON.parse(readFileSync(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; - } -} - -function getPackageManager(source: JsonValue | undefined): PackageManager | undefined { - if (source && isJsonObject(source)) { - const value = source['packageManager']; - if (typeof value === 'string') { - return value.split('@', 1)[0] as PackageManager; - } - } - - return undefined; -}