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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
parseNpmLikeManifest,
parseNpmLikeMetadata,
parseYarnClassicDependencies,
parseYarnLegacyManifest,
parseYarnClassicManifest,
parseYarnClassicMetadata,
parseYarnModernDependencies,
} from './parsers';

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

/** Whether a specific version lookup is needed prior to fetching a registry manifest. */
readonly requiresManifestVersionLookup?: boolean;

/** A function that formats the arguments for field-filtered registry views. */
readonly viewCommandFieldArgFormatter?: (fields: readonly string[]) => string[];

Expand Down Expand Up @@ -166,10 +170,11 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
versionCommand: ['--version'],
listDependenciesCommand: ['list', '--depth=0', '--json'],
getManifestCommand: ['info', '--json'],
requiresManifestVersionLookup: true,
outputParsers: {
listDependencies: parseYarnClassicDependencies,
getRegistryManifest: parseYarnLegacyManifest,
getRegistryMetadata: parseNpmLikeMetadata,
getRegistryManifest: parseYarnClassicManifest,
getRegistryMetadata: parseYarnClassicMetadata,
},
},
pnpm: {
Expand Down
30 changes: 27 additions & 3 deletions packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import { join } from 'node:path';
import npa from 'npm-package-arg';
import { maxSatisfying } from 'semver';
import { PackageManagerError } from './error';
import { Host } from './host';
import { Logger } from './logger';
Expand Down Expand Up @@ -156,12 +157,14 @@ export class PackageManager {
return { stdout: '', stderr: '' };
}

return this.host.runCommand(this.descriptor.binary, finalArgs, {
const commandResult = await this.host.runCommand(this.descriptor.binary, finalArgs, {
...runOptions,
cwd: executionDirectory,
stdio: 'pipe',
env: finalEnv,
});

return { stdout: commandResult.stdout.trim(), stderr: commandResult.stderr.trim() };
}

/**
Expand Down Expand Up @@ -395,13 +398,34 @@ export class PackageManager {
switch (type) {
case 'range':
case 'version':
case 'tag':
case 'tag': {
if (!name) {
throw new Error(`Could not parse package name from specifier: ${specifier}`);
}

// `fetchSpec` is the version, range, or tag.
return this.getRegistryManifest(name, fetchSpec ?? 'latest', options);
let versionSpec = fetchSpec ?? 'latest';
if (this.descriptor.requiresManifestVersionLookup) {
if (type === 'tag' || !fetchSpec) {
const metadata = await this.getRegistryMetadata(name, options);
if (!metadata) {
return null;
}
versionSpec = metadata['dist-tags'][versionSpec];
} else if (type === 'range') {
const metadata = await this.getRegistryMetadata(name, options);
if (!metadata) {
return null;
}
versionSpec = maxSatisfying(metadata.versions, fetchSpec) ?? '';
}
if (!versionSpec) {
return null;
}
}

return this.getRegistryManifest(name, versionSpec, options);
}
case 'directory': {
if (!fetchSpec) {
throw new Error(`Could not parse directory path from specifier: ${specifier}`);
Expand Down
44 changes: 41 additions & 3 deletions packages/angular/cli/src/package-managers/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,12 @@ export function parseNpmLikeMetadata(stdout: string, logger?: Logger): PackageMe
}

/**
* Parses the output of `yarn info` (classic).
* Parses the output of `yarn info` (classic) to get a package manifest.
* @param stdout The standard output of the command.
* @param logger An optional logger instance.
* @returns The package manifest object.
*/
export function parseYarnLegacyManifest(stdout: string, logger?: Logger): PackageManifest | null {
export function parseYarnClassicManifest(stdout: string, logger?: Logger): PackageManifest | null {
logger?.debug(`Parsing yarn classic manifest...`);
logStdout(stdout, logger);

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

// Yarn classic wraps the manifest in a `data` property.
return data.data ?? data;
const manifest = data.data as PackageManifest;

// Yarn classic removes any field with a falsy value
// https://github.com/yarnpkg/yarn/blob/7cafa512a777048ce0b666080a24e80aae3d66a9/src/cli/commands/info.js#L26-L29
// Add a default of 'false' for the `save` field when the `ng-add` object is present but does not have any fields.
// There is a small chance this causes an incorrect value. However, the use of `ng-add` is rare and, in the cases
// it is used, save is set to either a `false` literal or a truthy value. Special cases can be added for specific
// packages if discovered.
if (
manifest['ng-add'] &&
typeof manifest['ng-add'] === 'object' &&
Object.keys(manifest['ng-add']).length === 0
) {
manifest['ng-add'].save ??= false;
}

return manifest;
}

/**
* Parses the output of `yarn info` (classic) to get package metadata.
* @param stdout The standard output of the command.
* @param logger An optional logger instance.
* @returns The package metadata object.
*/
export function parseYarnClassicMetadata(stdout: string, logger?: Logger): PackageMetadata | null {
logger?.debug(`Parsing yarn classic metadata...`);
logStdout(stdout, logger);

if (!stdout) {
logger?.debug(' stdout is empty. No metadata found.');

return null;
}

const data = JSON.parse(stdout);

// Yarn classic wraps the metadata in a `data` property.
return data.data;
}