diff --git a/packages/angular/cli/src/package-managers/factory.ts b/packages/angular/cli/src/package-managers/factory.ts index e3635ae7b30f..ed15fcb8a05c 100644 --- a/packages/angular/cli/src/package-managers/factory.ts +++ b/packages/angular/cli/src/package-managers/factory.ts @@ -145,17 +145,18 @@ export async function createPackageManager(options: { } // Do not verify if the package manager is installed during a dry run. + let initializationError: Error | undefined; if (!dryRun && !version) { try { version = await getPackageManagerVersion(host, cwd, name, logger); } catch { if (source === 'default') { - throw new Error( + initializationError = new Error( `'${DEFAULT_PACKAGE_MANAGER}' was selected as the default package manager, but it is not installed or` + ` cannot be found in the PATH. Please install '${DEFAULT_PACKAGE_MANAGER}' to continue.`, ); } else { - throw new Error( + initializationError = new Error( `The project is configured to use '${name}', but it is not installed or cannot be` + ` found in the PATH. Please install '${name}' to continue.`, ); @@ -168,6 +169,7 @@ export async function createPackageManager(options: { logger, tempDirectory, version, + initializationError, }); logger?.debug(`Successfully created PackageManager for '${name}'.`); diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 7be28fc108f2..fd3b0a663a79 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -71,6 +71,12 @@ export interface PackageManagerOptions { * instead of running the version command. */ version?: string; + + /** + * An error that occurred during the initialization of the package manager. + * If provided, this error will be thrown when attempting to execute any command. + */ + initializationError?: Error; } /** @@ -84,6 +90,7 @@ export interface PackageManagerOptions { export class PackageManager { readonly #manifestCache = new Map(); readonly #metadataCache = new Map(); + readonly #initializationError?: Error; #dependencyCache: Map | null = null; #version: string | undefined; @@ -104,6 +111,7 @@ export class PackageManager { throw new Error('A logger must be provided when dryRun is enabled.'); } this.#version = options.version; + this.#initializationError = options.initializationError; } /** @@ -113,6 +121,18 @@ export class PackageManager { return this.descriptor.binary; } + /** + * Ensures that the package manager is installed and available in the PATH. + * If it is not, this method will throw an error with instructions on how to install it. + * + * @throws {Error} If the package manager is not installed. + */ + ensureInstalled(): void { + if (this.#initializationError) { + throw this.#initializationError; + } + } + /** * A private method to lazily populate the dependency cache. * This is a performance optimization to avoid running `npm list` multiple times. @@ -142,6 +162,8 @@ export class PackageManager { args: readonly string[], options: { timeout?: number; registry?: string; cwd?: string } = {}, ): Promise<{ stdout: string; stderr: string }> { + this.ensureInstalled(); + const { registry, cwd, ...runOptions } = options; const finalArgs = [...args]; let finalEnv: Record | undefined; diff --git a/packages/angular/cli/src/package-managers/package-manager_spec.ts b/packages/angular/cli/src/package-managers/package-manager_spec.ts index 2482349b323d..802f50fa66ab 100644 --- a/packages/angular/cli/src/package-managers/package-manager_spec.ts +++ b/packages/angular/cli/src/package-managers/package-manager_spec.ts @@ -51,4 +51,32 @@ describe('PackageManager', () => { expect(runCommandSpy).not.toHaveBeenCalled(); }); }); + + describe('initializationError', () => { + it('should throw initializationError when running commands', async () => { + const error = new Error('Not installed'); + const pm = new PackageManager(host, '/tmp', descriptor, { initializationError: error }); + + expect(() => pm.ensureInstalled()).toThrow(error); + await expectAsync(pm.getVersion()).toBeRejectedWith(error); + await expectAsync(pm.install()).toBeRejectedWith(error); + await expectAsync(pm.add('foo', 'none', false, false, false)).toBeRejectedWith(error); + }); + + it('should not throw initializationError for operations that do not require the binary', async () => { + const error = new Error('Not installed'); + const pm = new PackageManager(host, '/tmp', descriptor, { initializationError: error }); + + // Mock readFile for getManifest directory case + spyOn(host, 'readFile').and.resolveTo('{"name": "foo", "version": "1.0.0"}'); + + // Should not throw + const manifest = await pm.getManifest({ + type: 'directory', + fetchSpec: '/tmp/foo', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + expect(manifest).toEqual({ name: 'foo', version: '1.0.0' }); + }); + }); });