Skip to content
Merged
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
6 changes: 4 additions & 2 deletions packages/angular/cli/src/package-managers/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
);
Expand All @@ -168,6 +169,7 @@ export async function createPackageManager(options: {
logger,
tempDirectory,
version,
initializationError,
});

logger?.debug(`Successfully created PackageManager for '${name}'.`);
Expand Down
22 changes: 22 additions & 0 deletions packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -84,6 +90,7 @@ export interface PackageManagerOptions {
export class PackageManager {
readonly #manifestCache = new Map<string, PackageManifest | null>();
readonly #metadataCache = new Map<string, PackageMetadata | null>();
readonly #initializationError?: Error;
#dependencyCache: Map<string, InstalledPackage> | null = null;
#version: string | undefined;

Expand All @@ -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;
}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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<string, string> | undefined;
Expand Down
28 changes: 28 additions & 0 deletions packages/angular/cli/src/package-managers/package-manager_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
});
});
});