Skip to content

Commit 9319949

Browse files
committed
refactor(@angular/cli): implement lazy validation for package manager
This changes the package manager initialization to only throw errors when the binary is actually required for an operation. This allows CLI commands that do not depend on the package manager binary to function even if the configured package manager is missing.
1 parent b33678e commit 9319949

File tree

3 files changed

+43
-2
lines changed

3 files changed

+43
-2
lines changed

packages/angular/cli/src/package-managers/factory.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,18 @@ export async function createPackageManager(options: {
145145
}
146146

147147
// Do not verify if the package manager is installed during a dry run.
148+
let initializationError: Error | undefined;
148149
if (!dryRun && !version) {
149150
try {
150151
version = await getPackageManagerVersion(host, cwd, name, logger);
151152
} catch {
152153
if (source === 'default') {
153-
throw new Error(
154+
initializationError = new Error(
154155
`'${DEFAULT_PACKAGE_MANAGER}' was selected as the default package manager, but it is not installed or` +
155156
` cannot be found in the PATH. Please install '${DEFAULT_PACKAGE_MANAGER}' to continue.`,
156157
);
157158
} else {
158-
throw new Error(
159+
initializationError = new Error(
159160
`The project is configured to use '${name}', but it is not installed or cannot be` +
160161
` found in the PATH. Please install '${name}' to continue.`,
161162
);
@@ -168,6 +169,7 @@ export async function createPackageManager(options: {
168169
logger,
169170
tempDirectory,
170171
version,
172+
initializationError,
171173
});
172174

173175
logger?.debug(`Successfully created PackageManager for '${name}'.`);

packages/angular/cli/src/package-managers/package-manager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export interface PackageManagerOptions {
7171
* instead of running the version command.
7272
*/
7373
version?: string;
74+
75+
/**
76+
* An error that occurred during the initialization of the package manager.
77+
* If provided, this error will be thrown when attempting to execute any command.
78+
*/
79+
initializationError?: Error;
7480
}
7581

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

@@ -104,6 +111,7 @@ export class PackageManager {
104111
throw new Error('A logger must be provided when dryRun is enabled.');
105112
}
106113
this.#version = options.version;
114+
this.#initializationError = options.initializationError;
107115
}
108116

109117
/**
@@ -142,6 +150,10 @@ export class PackageManager {
142150
args: readonly string[],
143151
options: { timeout?: number; registry?: string; cwd?: string } = {},
144152
): Promise<{ stdout: string; stderr: string }> {
153+
if (this.#initializationError) {
154+
throw this.#initializationError;
155+
}
156+
145157
const { registry, cwd, ...runOptions } = options;
146158
const finalArgs = [...args];
147159
let finalEnv: Record<string, string> | undefined;

packages/angular/cli/src/package-managers/package-manager_spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,31 @@ describe('PackageManager', () => {
5151
expect(runCommandSpy).not.toHaveBeenCalled();
5252
});
5353
});
54+
55+
describe('initializationError', () => {
56+
it('should throw initializationError when running commands', async () => {
57+
const error = new Error('Not installed');
58+
const pm = new PackageManager(host, '/tmp', descriptor, { initializationError: error });
59+
60+
await expectAsync(pm.getVersion()).toBeRejectedWith(error);
61+
await expectAsync(pm.install()).toBeRejectedWith(error);
62+
await expectAsync(pm.add('foo', 'none', false, false, false)).toBeRejectedWith(error);
63+
});
64+
65+
it('should not throw initializationError for operations that do not require the binary', async () => {
66+
const error = new Error('Not installed');
67+
const pm = new PackageManager(host, '/tmp', descriptor, { initializationError: error });
68+
69+
// Mock readFile for getManifest directory case
70+
spyOn(host, 'readFile').and.resolveTo('{"name": "foo", "version": "1.0.0"}');
71+
72+
// Should not throw
73+
const manifest = await pm.getManifest({
74+
type: 'directory',
75+
fetchSpec: '/tmp/foo',
76+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
77+
} as any);
78+
expect(manifest).toEqual({ name: 'foo', version: '1.0.0' });
79+
});
80+
});
5481
});

0 commit comments

Comments
 (0)