Skip to content

Commit d8b76e9

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular/cli): correctly handle yarn classic tag manifest fetching
Introduces a `requiresManifestVersionLookup` property to `PackageManagerDescriptor` to control whether a package manager needs an explicit metadata lookup for tags, ranges, 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 and ranges to concrete versions.
1 parent 51055fb commit d8b76e9

File tree

3 files changed

+76
-9
lines changed

3 files changed

+76
-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 specific version lookup is needed prior to fetching a registry manifest. */
78+
readonly requiresManifestVersionLookup?: 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+
requiresManifestVersionLookup: 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: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { join } from 'node:path';
1616
import npa from 'npm-package-arg';
17+
import { maxSatisfying } from 'semver';
1718
import { PackageManagerError } from './error';
1819
import { Host } from './host';
1920
import { Logger } from './logger';
@@ -156,12 +157,14 @@ export class PackageManager {
156157
return { stdout: '', stderr: '' };
157158
}
158159

159-
return this.host.runCommand(this.descriptor.binary, finalArgs, {
160+
const commandResult = await this.host.runCommand(this.descriptor.binary, finalArgs, {
160161
...runOptions,
161162
cwd: executionDirectory,
162163
stdio: 'pipe',
163164
env: finalEnv,
164165
});
166+
167+
return { stdout: commandResult.stdout.trim(), stderr: commandResult.stderr.trim() };
165168
}
166169

167170
/**
@@ -395,13 +398,34 @@ export class PackageManager {
395398
switch (type) {
396399
case 'range':
397400
case 'version':
398-
case 'tag':
401+
case 'tag': {
399402
if (!name) {
400403
throw new Error(`Could not parse package name from specifier: ${specifier}`);
401404
}
402405

403406
// `fetchSpec` is the version, range, or tag.
404-
return this.getRegistryManifest(name, fetchSpec ?? 'latest', options);
407+
let versionSpec = fetchSpec ?? 'latest';
408+
if (this.descriptor.requiresManifestVersionLookup) {
409+
if (type === 'tag' || !fetchSpec) {
410+
const metadata = await this.getRegistryMetadata(name, options);
411+
if (!metadata) {
412+
return null;
413+
}
414+
versionSpec = metadata['dist-tags'][versionSpec];
415+
} else if (type === 'range') {
416+
const metadata = await this.getRegistryMetadata(name, options);
417+
if (!metadata) {
418+
return null;
419+
}
420+
versionSpec = maxSatisfying(metadata.versions, fetchSpec) ?? '';
421+
}
422+
if (!versionSpec) {
423+
return null;
424+
}
425+
}
426+
427+
return this.getRegistryManifest(name, versionSpec, options);
428+
}
405429
case 'directory': {
406430
if (!fetchSpec) {
407431
throw new Error(`Could not parse directory path from specifier: ${specifier}`);

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

Lines changed: 41 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,43 @@ 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 (
299+
manifest['ng-add'] &&
300+
typeof manifest['ng-add'] === 'object' &&
301+
Object.keys(manifest['ng-add']).length === 0
302+
) {
303+
manifest['ng-add'].save ??= false;
304+
}
305+
306+
return manifest;
307+
}
308+
309+
/**
310+
* Parses the output of `yarn info` (classic) to get package metadata.
311+
* @param stdout The standard output of the command.
312+
* @param logger An optional logger instance.
313+
* @returns The package metadata object.
314+
*/
315+
export function parseYarnClassicMetadata(stdout: string, logger?: Logger): PackageMetadata | null {
316+
logger?.debug(`Parsing yarn classic metadata...`);
317+
logStdout(stdout, logger);
318+
319+
if (!stdout) {
320+
logger?.debug(' stdout is empty. No metadata found.');
321+
322+
return null;
323+
}
324+
325+
const data = JSON.parse(stdout);
326+
327+
// Yarn classic wraps the metadata in a `data` property.
328+
return data.data;
291329
}

0 commit comments

Comments
 (0)