@angular/cli
Version:
CLI tool for Angular
304 lines • 14 kB
JavaScript
"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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PackageManager = void 0;
/**
* @fileoverview This file contains the `PackageManager` class, which is the
* core execution engine for all package manager commands. It is designed to be
* a flexible and secure abstraction over the various package managers.
*/
const node_path_1 = require("node:path");
const error_1 = require("./error");
/**
* The fields to request from the registry for package metadata.
* This is a performance optimization to avoid downloading the full manifest
* when only summary data (like versions and tags) is needed.
*/
const METADATA_FIELDS = ['name', 'dist-tags', 'versions', 'time'];
/**
* The fields to request from the registry for a package's manifest.
* This is a performance optimization to avoid downloading unnecessary data.
* These fields are the ones required by the CLI for operations like `ng add` and `ng update`.
*/
const MANIFEST_FIELDS = [
'name',
'version',
'deprecated',
'dependencies',
'peerDependencies',
'devDependencies',
'homepage',
'schematics',
'ng-add',
'ng-update',
];
/**
* A class that provides a high-level, package-manager-agnostic API for
* interacting with a project's dependencies.
*
* This class is an implementation of the Strategy design pattern. It is
* instantiated with a `PackageManagerDescriptor` that defines the specific
* commands and flags for a given package manager.
*/
class PackageManager {
host;
cwd;
descriptor;
options;
#manifestCache = new Map();
#metadataCache = new Map();
#dependencyCache = null;
/**
* Creates a new `PackageManager` instance.
* @param host A `Host` instance for interacting with the file system and running commands.
* @param cwd The absolute path to the project's working directory.
* @param descriptor A `PackageManagerDescriptor` that defines the commands for a specific package manager.
* @param options An options object to configure the instance.
*/
constructor(host, cwd, descriptor, options = {}) {
this.host = host;
this.cwd = cwd;
this.descriptor = descriptor;
this.options = options;
if (this.options.dryRun && !this.options.logger) {
throw new Error('A logger must be provided when dryRun is enabled.');
}
}
/**
* The name of the package manager's binary.
*/
get name() {
return this.descriptor.binary;
}
/**
* A private method to lazily populate the dependency cache.
* This is a performance optimization to avoid running `npm list` multiple times.
* @returns A promise that resolves to the dependency cache map.
*/
async #populateDependencyCache() {
if (this.#dependencyCache !== null) {
return this.#dependencyCache;
}
const args = this.descriptor.listDependenciesCommand;
const dependencies = await this.#fetchAndParse(args, (stdout, logger) => this.descriptor.outputParsers.listDependencies(stdout, logger));
return (this.#dependencyCache = dependencies ?? new Map());
}
/**
* A private method to run a command using the package manager's binary.
* @param args The arguments to pass to the command.
* @param options Options for the child process.
* @returns A promise that resolves with the standard output and standard error of the command.
*/
async #run(args, options = {}) {
const { registry, cwd, ...runOptions } = options;
const finalArgs = [...args];
let finalEnv;
if (registry) {
const registryOptions = this.descriptor.getRegistryOptions?.(registry);
if (!registryOptions) {
throw new Error(`The configured package manager, '${this.descriptor.binary}', does not support a custom registry.`);
}
if (registryOptions.args) {
finalArgs.push(...registryOptions.args);
}
if (registryOptions.env) {
finalEnv = registryOptions.env;
}
}
const executionDirectory = cwd ?? this.cwd;
if (this.options.dryRun) {
this.options.logger?.info(`[DRY RUN] Would execute in [${executionDirectory}]: ${this.descriptor.binary} ${finalArgs.join(' ')}`);
return { stdout: '', stderr: '' };
}
return this.host.runCommand(this.descriptor.binary, finalArgs, {
...runOptions,
cwd: executionDirectory,
stdio: 'pipe',
env: finalEnv,
});
}
/**
* A private, generic method to encapsulate the common logic of running a command,
* handling errors, and parsing the output.
* @param args The arguments to pass to the command.
* @param parser A function that parses the command's stdout.
* @param options Options for the command, including caching.
* @returns A promise that resolves to the parsed data, or null if not found.
*/
async #fetchAndParse(args, parser, options = {}) {
const { cache, cacheKey, bypassCache, ...runOptions } = options;
if (!bypassCache && cache && cacheKey && cache.has(cacheKey)) {
return cache.get(cacheKey);
}
let stdout;
let stderr;
try {
({ stdout, stderr } = await this.#run(args, runOptions));
}
catch (e) {
if (e instanceof error_1.PackageManagerError && typeof e.exitCode === 'number' && e.exitCode !== 0) {
// Some package managers exit with a non-zero code when the package is not found.
if (cache && cacheKey) {
cache.set(cacheKey, null);
}
return null;
}
throw e;
}
try {
const result = parser(stdout, this.options.logger);
if (cache && cacheKey) {
cache.set(cacheKey, result);
}
return result;
}
catch (e) {
const message = `Failed to parse package manager output: ${e instanceof Error ? e.message : ''}`;
throw new error_1.PackageManagerError(message, stdout, stderr, 0);
}
}
/**
* Adds a package to the project's dependencies.
* @param packageName The name of the package to add.
* @param save The save strategy to use.
* - `exact`: The package will be saved with an exact version.
* - `tilde`: The package will be saved with a tilde version range (`~`).
* - `none`: The package will be saved with the default version range (`^`).
* @param asDevDependency Whether to install the package as a dev dependency.
* @param noLockfile Whether to skip updating the lockfile.
* @param options Extra options for the command.
* @returns A promise that resolves when the command is complete.
*/
async add(packageName, save, asDevDependency, noLockfile, ignoreScripts, options = {}) {
const flags = [
asDevDependency ? this.descriptor.saveDevFlag : '',
save === 'exact' ? this.descriptor.saveExactFlag : '',
save === 'tilde' ? this.descriptor.saveTildeFlag : '',
noLockfile ? this.descriptor.noLockfileFlag : '',
ignoreScripts ? this.descriptor.ignoreScriptsFlag : '',
].filter((flag) => flag);
const args = [this.descriptor.addCommand, packageName, ...flags];
await this.#run(args, options);
this.#dependencyCache = null;
}
/**
* Installs all dependencies in the project.
* @param options Options for the installation.
* @param options.timeout The maximum time in milliseconds to wait for the command to complete.
* @param options.force If true, forces a clean install, potentially overwriting existing modules.
* @param options.registry The registry to use for the installation.
* @param options.ignoreScripts If true, prevents lifecycle scripts from being executed.
* @returns A promise that resolves when the command is complete.
*/
async install(options = { ignoreScripts: true }) {
const flags = [
options.force ? this.descriptor.forceFlag : '',
options.ignoreScripts ? this.descriptor.ignoreScriptsFlag : '',
].filter((flag) => flag);
const args = [...this.descriptor.installCommand, ...flags];
await this.#run(args, options);
this.#dependencyCache = null;
}
/**
* Gets the version of the package manager binary.
* @returns A promise that resolves to the trimmed version string.
*/
async getVersion() {
const { stdout } = await this.#run(this.descriptor.versionCommand);
return stdout.trim();
}
/**
* Gets the installed details of a package from the project's dependencies.
* @param packageName The name of the package to check.
* @returns A promise that resolves to the installed package details, or `null` if the package is not installed.
*/
async getInstalledPackage(packageName) {
const cache = await this.#populateDependencyCache();
return cache.get(packageName) ?? null;
}
/**
* Gets a map of all top-level dependencies installed in the project.
* @returns A promise that resolves to a map of package names to their installed package details.
*/
async getProjectDependencies() {
const cache = await this.#populateDependencyCache();
// Return a copy to prevent external mutations of the cache.
return new Map(cache);
}
/**
* Fetches the registry metadata for a package. This is the full metadata,
* including all versions and distribution tags.
* @param packageName The name of the package to fetch the metadata for.
* @param options Options for the fetch.
* @param options.timeout The maximum time in milliseconds to wait for the command to complete.
* @param options.registry The registry to use for the fetch.
* @param options.bypassCache If true, ignores the in-memory cache and fetches fresh data.
* @returns A promise that resolves to the `PackageMetadata` object, or `null` if the package is not found.
*/
async getRegistryMetadata(packageName, options = {}) {
const commandArgs = [...this.descriptor.getManifestCommand, packageName];
const formatter = this.descriptor.viewCommandFieldArgFormatter;
if (formatter) {
commandArgs.push(...formatter(METADATA_FIELDS));
}
const cacheKey = options.registry ? `${packageName}|${options.registry}` : packageName;
return this.#fetchAndParse(commandArgs, (stdout, logger) => this.descriptor.outputParsers.getRegistryMetadata(stdout, logger), { ...options, cache: this.#metadataCache, cacheKey });
}
/**
* Fetches the registry manifest for a specific version of a package.
* The manifest is similar to the package's `package.json` file.
* @param packageName The name of the package to fetch the manifest for.
* @param version The version of the package to fetch the manifest for.
* @param options Options for the fetch.
* @param options.timeout The maximum time in milliseconds to wait for the command to complete.
* @param options.registry The registry to use for the fetch.
* @param options.bypassCache If true, ignores the in-memory cache and fetches fresh data.
* @returns A promise that resolves to the `PackageManifest` object, or `null` if the package is not found.
*/
async getPackageManifest(packageName, version, options = {}) {
const specifier = `${packageName}@${version}`;
const commandArgs = [...this.descriptor.getManifestCommand, specifier];
const formatter = this.descriptor.viewCommandFieldArgFormatter;
if (formatter) {
commandArgs.push(...formatter(MANIFEST_FIELDS));
}
const cacheKey = options.registry ? `${specifier}|${options.registry}` : specifier;
return this.#fetchAndParse(commandArgs, (stdout, logger) => this.descriptor.outputParsers.getPackageManifest(stdout, logger), { ...options, cache: this.#manifestCache, cacheKey });
}
/**
* Acquires a package by installing it into a temporary directory. The caller is
* responsible for managing the lifecycle of the temporary directory by calling
* the returned `cleanup` function.
*
* @param packageName The name of the package to install.
* @param options Options for the installation.
* @returns A promise that resolves to an object containing the temporary path
* and a cleanup function.
*/
async acquireTempPackage(packageName, options = {}) {
const workingDirectory = await this.host.createTempDirectory();
const cleanup = () => this.host.deleteDirectory(workingDirectory);
// Some package managers, like yarn classic, do not write a package.json when adding a package.
// This can cause issues with subsequent `require.resolve` calls.
// Writing an empty package.json file beforehand prevents this.
await this.host.writeFile((0, node_path_1.join)(workingDirectory, 'package.json'), '{}');
const args = [this.descriptor.addCommand, packageName];
try {
await this.#run(args, { ...options, cwd: workingDirectory });
}
catch (e) {
// If the command fails, clean up the temporary directory immediately.
await cleanup();
throw e;
}
return { workingDirectory, cleanup };
}
}
exports.PackageManager = PackageManager;
//# sourceMappingURL=package-manager.js.map