Skip to content
Open
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 @@ -87,6 +87,9 @@ export interface PackageManagerDescriptor {
/** The command to list all installed dependencies. */
readonly listDependenciesCommand: readonly string[];

/** The command to get the current package name. */
readonly getPackageNameCommand?: readonly string[];

/** The command to fetch the registry manifest of a package. */
readonly getManifestCommand: readonly string[];

Expand All @@ -99,7 +102,11 @@ export interface PackageManagerDescriptor {
/** A collection of functions to parse the output of specific commands. */
readonly outputParsers: {
/** A function to parse the output of `listDependenciesCommand`. */
listDependencies: (stdout: string, logger?: Logger) => Map<string, InstalledPackage>;
listDependencies: (
stdout: string,
logger?: Logger,
options?: { workspacePackageName?: string },
) => Map<string, InstalledPackage>;

/** A function to parse the output of `getManifestCommand` for a specific version. */
getRegistryManifest: (stdout: string, logger?: Logger) => PackageManifest | null;
Expand Down Expand Up @@ -158,6 +165,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }),
versionCommand: ['--version'],
listDependenciesCommand: ['list', '--depth=0', '--json=true', '--all=true'],
getPackageNameCommand: ['pkg', 'get', 'name'],
getManifestCommand: ['view', '--json'],
viewCommandFieldArgFormatter: (fields) => [...fields],
outputParsers: {
Expand Down Expand Up @@ -237,6 +245,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }),
versionCommand: ['--version'],
listDependenciesCommand: ['list', '--depth=0', '--json'],
getPackageNameCommand: ['pkg', 'get', 'name'],
getManifestCommand: ['view', '--json'],
viewCommandFieldArgFormatter: (fields) => [...fields],
outputParsers: {
Expand Down
28 changes: 27 additions & 1 deletion packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ export class PackageManager {

const args = this.descriptor.listDependenciesCommand;

const workspacePackageName = await this.getCurrentPackageName();
const dependencies = await this.#fetchAndParse(args, (stdout, logger) =>
this.descriptor.outputParsers.listDependencies(stdout, logger),
this.descriptor.outputParsers.listDependencies(stdout, logger, { workspacePackageName }),
);

return (this.#dependencyCache = dependencies ?? new Map());
Expand Down Expand Up @@ -361,6 +362,31 @@ export class PackageManager {
this.#dependencyCache = null;
}

/**
* Gets the name of the package in the current project.
*/
async getCurrentPackageName(): Promise<string | undefined> {
if (this.descriptor.getPackageNameCommand) {
try {
const { stdout } = await this.#run(this.descriptor.getPackageNameCommand);
if (stdout) {
return JSON.parse(stdout);
}
} catch {
// Fall back to reading file if command fails
}
}

try {
const content = await this.host.readFile(join(this.cwd, 'package.json'));
const pkgJson = JSON.parse(content);

return pkgJson.name;
} catch {
return undefined;
}
}
Comment thread
clydin marked this conversation as resolved.

/**
* Gets the version of the package manager binary.
*/
Expand Down
56 changes: 50 additions & 6 deletions packages/angular/cli/src/package-managers/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ interface NpmListDependency {
export function parseNpmLikeDependencies(
stdout: string,
logger?: Logger,
options?: { workspacePackageName?: string },
): Map<string, InstalledPackage> {
logger?.debug(`Parsing npm-like dependency list...`);
logStdout(stdout, logger);
Expand Down Expand Up @@ -108,13 +109,56 @@ export function parseNpmLikeDependencies(
return dependencies;
}

const workspacePackageName = options?.workspacePackageName;

if (workspacePackageName) {
for (const dependencyMap of dependencyMaps) {
const info = dependencyMap[workspacePackageName];
if (info && typeof info === 'object') {
const nestedMaps = [
info.dependencies,
info.devDependencies,
info.unsavedDependencies,
].filter((d) => !!d) as Record<string, NpmListDependency>[];

for (const nestedMap of nestedMaps) {
for (const [name, nestedInfo] of Object.entries(nestedMap)) {
if (nestedInfo && typeof nestedInfo === 'object' && nestedInfo.version) {
dependencies.set(name, {
name,
version: nestedInfo.version,
path: nestedInfo.path,
});
}
}
}
}
}
}

// Extract top-level dependencies (root), without overwriting subproject dependencies
for (const dependencyMap of dependencyMaps) {
for (const [name, info] of Object.entries(dependencyMap as Record<string, NpmListDependency>)) {
dependencies.set(name, {
name,
version: info.version,
path: info.path,
});
for (const [name, info] of Object.entries(
dependencyMap as Record<string, NpmListDependency & { resolved?: string }>,
)) {
if (!info || typeof info !== 'object') {
continue;
}

// Exclude local monorepo workspace packages (which originate from a local file/dir
// and contain nested dependency maps in the output of `npm list --depth=0`),
// while preserving third-party packages installed from local paths.
const isWorkspacePackage =
info.resolved?.startsWith('file:') &&
(!!info.dependencies || !!info.devDependencies || !!info.unsavedDependencies);

if (info.version && !dependencies.has(name) && !isWorkspacePackage) {
dependencies.set(name, {
name,
version: info.version,
path: info.path,
});
}
}
}

Expand Down
46 changes: 46 additions & 0 deletions packages/angular/cli/src/package-managers/parsers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {
parseBunDependencies,
parseNpmLikeDependencies,
parseNpmLikeError,
parseNpmLikeManifest,
parseYarnClassicDependencies,
Expand All @@ -16,6 +17,51 @@ import {
} from './parsers';

describe('parsers', () => {
describe('parseNpmLikeDependencies', () => {
it('should parse simple dependencies', () => {
const stdout = JSON.stringify({
dependencies: {
rxjs: {
version: '7.8.2',
},
},
});
const deps = parseNpmLikeDependencies(stdout);
expect(deps.size).toBe(1);
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2', path: undefined });
});

it('should parse dependencies from current subproject and hoisted root', () => {
const stdout = JSON.stringify({
version: '1.0.0',
name: 'monorepo-root',
dependencies: {
app: {
version: '1.0.0',
resolved: 'file:../packages/app',
dependencies: {
rxjs: {
version: '7.8.1',
},
},
},
typescript: {
version: '5.9.3',
},
},
});

const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'app' });
expect(deps.size).toBe(2);
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.1', path: undefined });
expect(deps.get('typescript')).toEqual({
name: 'typescript',
version: '5.9.3',
path: undefined,
});
});
});

describe('parseNpmLikeError', () => {
it('should parse a structured JSON error from modern yarn', () => {
const stdout = JSON.stringify({
Expand Down
Loading