Skip to content

Commit 51bee53

Browse files
committed
fix(@angular/cli): correctly handle yarn classic tag manifest fetching
Introduces a `requiresManifestTagLookup` property to `PackageManagerDescriptor` to control whether a package manager needs an explicit metadata lookup for tags (or when no `fetchSpec` is provided) before attempting to fetch the full registry manifest. This change optimizes manifest fetching by enabling a preliminary metadata lookup for package managers like `yarn-classic` that require it to resolve tags to concrete versions.
1 parent b6eb594 commit 51bee53

File tree

3 files changed

+63
-9
lines changed

3 files changed

+63
-9
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
parseNpmLikeManifest,
2121
parseNpmLikeMetadata,
2222
parseYarnClassicDependencies,
23-
parseYarnLegacyManifest,
23+
parseYarnClassicManifest,
24+
parseYarnClassicMetadata,
2425
parseYarnModernDependencies,
2526
} from './parsers';
2627

@@ -73,6 +74,9 @@ export interface PackageManagerDescriptor {
7374
/** The command to fetch the registry manifest of a package. */
7475
readonly getManifestCommand: readonly string[];
7576

77+
/** Whether a tag needs a lookup prior to fetching a registry manifest. */
78+
readonly requiresManifestTagLookup?: boolean;
79+
7680
/** A function that formats the arguments for field-filtered registry views. */
7781
readonly viewCommandFieldArgFormatter?: (fields: readonly string[]) => string[];
7882

@@ -166,10 +170,11 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
166170
versionCommand: ['--version'],
167171
listDependenciesCommand: ['list', '--depth=0', '--json'],
168172
getManifestCommand: ['info', '--json'],
173+
requiresManifestTagLookup: true,
169174
outputParsers: {
170175
listDependencies: parseYarnClassicDependencies,
171-
getRegistryManifest: parseYarnLegacyManifest,
172-
getRegistryMetadata: parseNpmLikeMetadata,
176+
getRegistryManifest: parseYarnClassicManifest,
177+
getRegistryMetadata: parseYarnClassicMetadata,
173178
},
174179
},
175180
pnpm: {

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,14 @@ export class PackageManager {
156156
return { stdout: '', stderr: '' };
157157
}
158158

159-
return this.host.runCommand(this.descriptor.binary, finalArgs, {
159+
const commandResult = await this.host.runCommand(this.descriptor.binary, finalArgs, {
160160
...runOptions,
161161
cwd: executionDirectory,
162162
stdio: 'pipe',
163163
env: finalEnv,
164164
});
165+
166+
return { stdout: commandResult.stdout.trim(), stderr: commandResult.stderr.trim() };
165167
}
166168

167169
/**
@@ -395,13 +397,26 @@ export class PackageManager {
395397
switch (type) {
396398
case 'range':
397399
case 'version':
398-
case 'tag':
400+
case 'tag': {
399401
if (!name) {
400402
throw new Error(`Could not parse package name from specifier: ${specifier}`);
401403
}
402404

403405
// `fetchSpec` is the version, range, or tag.
404-
return this.getRegistryManifest(name, fetchSpec ?? 'latest', options);
406+
let versionSpec = fetchSpec ?? 'latest';
407+
if ((type === 'tag' || !fetchSpec) && this.descriptor.requiresManifestTagLookup) {
408+
const metadata = await this.getRegistryMetadata(name, options);
409+
if (!metadata) {
410+
return null;
411+
}
412+
versionSpec = metadata['dist-tags'][versionSpec];
413+
if (!versionSpec) {
414+
return null;
415+
}
416+
}
417+
418+
return this.getRegistryManifest(name, versionSpec, options);
419+
}
405420
case 'directory': {
406421
if (!fetchSpec) {
407422
throw new Error(`Could not parse directory path from specifier: ${specifier}`);

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,12 @@ export function parseNpmLikeMetadata(stdout: string, logger?: Logger): PackageMe
269269
}
270270

271271
/**
272-
* Parses the output of `yarn info` (classic).
272+
* Parses the output of `yarn info` (classic) to get a package manifest.
273273
* @param stdout The standard output of the command.
274274
* @param logger An optional logger instance.
275275
* @returns The package manifest object.
276276
*/
277-
export function parseYarnLegacyManifest(stdout: string, logger?: Logger): PackageManifest | null {
277+
export function parseYarnClassicManifest(stdout: string, logger?: Logger): PackageManifest | null {
278278
logger?.debug(`Parsing yarn classic manifest...`);
279279
logStdout(stdout, logger);
280280

@@ -287,5 +287,39 @@ export function parseYarnLegacyManifest(stdout: string, logger?: Logger): Packag
287287
const data = JSON.parse(stdout);
288288

289289
// Yarn classic wraps the manifest in a `data` property.
290-
return data.data ?? data;
290+
const manifest = data.data as PackageManifest;
291+
292+
// Yarn classic removes any field with a falsy value
293+
// https://github.com/yarnpkg/yarn/blob/7cafa512a777048ce0b666080a24e80aae3d66a9/src/cli/commands/info.js#L26-L29
294+
// Add a default of 'false' for the `save` field when the `ng-add` object is present but does not have any fields.
295+
// There is a small chance this causes an incorrect value. However, the use of `ng-add` is rare and, in the cases
296+
// it is used, save is set to either a `false` literal or a truthy value. Special cases can be added for specific
297+
// packages if discovered.
298+
if (typeof manifest['ng-add'] === 'object' && Object.keys(manifest['ng-add']).length === 0) {
299+
manifest['ng-add'].save ??= false;
300+
}
301+
302+
return manifest;
303+
}
304+
305+
/**
306+
* Parses the output of `yarn info` (classic) to get package metadata.
307+
* @param stdout The standard output of the command.
308+
* @param logger An optional logger instance.
309+
* @returns The package metadata object.
310+
*/
311+
export function parseYarnClassicMetadata(stdout: string, logger?: Logger): PackageMetadata | null {
312+
logger?.debug(`Parsing yarn classic metadata...`);
313+
logStdout(stdout, logger);
314+
315+
if (!stdout) {
316+
logger?.debug(' stdout is empty. No metadata found.');
317+
318+
return null;
319+
}
320+
321+
const data = JSON.parse(stdout);
322+
323+
// Yarn classic wraps the metadata in a `data` property.
324+
return data.data;
291325
}

0 commit comments

Comments
 (0)