Skip to content

Commit db3934f

Browse files
committed
refactor(@angular/cli): improve update package discovery
1 parent 6c0ab83 commit db3934f

40 files changed

+841
-110
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"@types/pidusage": "^2.0.1",
116116
"@types/progress": "^2.0.3",
117117
"@types/request": "^2.47.1",
118+
"@types/resolve": "^1.17.1",
118119
"@types/rimraf": "^3.0.0",
119120
"@types/semver": "^7.0.0",
120121
"@types/universal-analytics": "^0.4.2",

packages/angular/cli/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ ts_library(
7272
"@npm//@types/debug",
7373
"@npm//@types/inquirer",
7474
"@npm//@types/node",
75+
"@npm//@types/resolve",
7576
"@npm//@types/rimraf",
7677
"@npm//@types/semver",
7778
"@npm//@types/universal-analytics",

packages/angular/cli/commands/update-impl.ts

+26-20
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ import {
2727
fetchPackageManifest,
2828
fetchPackageMetadata,
2929
} from '../utilities/package-metadata';
30-
import { PackageTreeNode, findNodeDependencies, readPackageTree } from '../utilities/package-tree';
30+
import {
31+
PackageTreeNode,
32+
findPackageJson,
33+
getProjectDependencies,
34+
readPackageJson,
35+
} from '../utilities/package-tree';
3136
import { Schema as UpdateCommandSchema } from './update';
3237

3338
const npa = require('npm-package-arg') as (selector: string) => PackageIdentifier;
@@ -377,15 +382,14 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
377382

378383
this.logger.info('Collecting installed dependencies...');
379384

380-
const packageTree = await readPackageTree(this.workspace.root);
381-
const rootDependencies = findNodeDependencies(packageTree);
385+
const rootDependencies = await getProjectDependencies(this.workspace.root);
382386

383-
this.logger.info(`Found ${Object.keys(rootDependencies).length} dependencies.`);
387+
this.logger.info(`Found ${rootDependencies.size} dependencies.`);
384388

385389
if (options.all) {
386390
// 'all' option and a zero length packages have already been checked.
387391
// Add all direct dependencies to be updated
388-
for (const dep of Object.keys(rootDependencies)) {
392+
for (const dep of rootDependencies.keys()) {
389393
const packageIdentifier = npa(dep);
390394
if (options.next) {
391395
packageIdentifier.fetchSpec = 'next';
@@ -400,7 +404,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
400404
next: options.next || false,
401405
verbose: options.verbose || false,
402406
packageManager: this.packageManager,
403-
packages: options.all ? Object.keys(rootDependencies) : [],
407+
packages: options.all ? rootDependencies.keys() : [],
404408
});
405409

406410
return success ? 0 : 1;
@@ -424,8 +428,9 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
424428
}
425429

426430
const packageName = packages[0].name;
427-
const packageDependency = rootDependencies[packageName];
428-
let packageNode = packageDependency && packageDependency.node;
431+
const packageDependency = rootDependencies.get(packageName);
432+
let packagePath = packageDependency?.path;
433+
let packageNode = packageDependency?.package;
429434
if (packageDependency && !packageNode) {
430435
this.logger.error('Package found in package.json but is not installed.');
431436

@@ -434,19 +439,20 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
434439
// Allow running migrations on transitively installed dependencies
435440
// There can technically be nested multiple versions
436441
// TODO: If multiple, this should find all versions and ask which one to use
437-
const child = packageTree.children.find(c => c.name === packageName);
438-
if (child) {
439-
packageNode = child;
442+
const packageJson = findPackageJson(this.workspace.root, packageName);
443+
if (packageJson) {
444+
packagePath = path.dirname(packageJson);
445+
packageNode = await readPackageJson(packagePath);
440446
}
441447
}
442448

443-
if (!packageNode) {
449+
if (!packageNode || !packagePath) {
444450
this.logger.error('Package is not installed.');
445451

446452
return 1;
447453
}
448454

449-
const updateMetadata = packageNode.package['ng-update'];
455+
const updateMetadata = packageNode['ng-update'];
450456
let migrations = updateMetadata && updateMetadata.migrations;
451457
if (migrations === undefined) {
452458
this.logger.error('Package does not provide migrations.');
@@ -477,14 +483,14 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
477483
}
478484

479485
// Check if it is a package-local location
480-
const localMigrations = path.join(packageNode.path, migrations);
486+
const localMigrations = path.join(packagePath, migrations);
481487
if (fs.existsSync(localMigrations)) {
482488
migrations = localMigrations;
483489
} else {
484490
// Try to resolve from package location.
485491
// This avoids issues with package hoisting.
486492
try {
487-
migrations = require.resolve(migrations, { paths: [packageNode.path] });
493+
migrations = require.resolve(migrations, { paths: [packagePath] });
488494
} catch (e) {
489495
if (e.code === 'MODULE_NOT_FOUND') {
490496
this.logger.error('Migrations for package were not found.');
@@ -513,7 +519,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
513519
}
514520

515521
const migrationRange = new semver.Range(
516-
'>' + from + ' <=' + (options.to || packageNode.package.version),
522+
'>' + from + ' <=' + (options.to || packageNode.version),
517523
);
518524

519525
success = await this.executeMigrations(
@@ -529,7 +535,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
529535
packageName === '@angular/core'
530536
&& options.from
531537
&& +options.from.split('.')[0] < 9
532-
&& (options.to || packageNode.package.version).split('.')[0] === '9'
538+
&& (options.to || packageNode.version).split('.')[0] === '9'
533539
) {
534540
this.logger.info(NG_VERSION_9_POST_MSG);
535541
}
@@ -547,8 +553,8 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
547553

548554
// Validate packages actually are part of the workspace
549555
for (const pkg of packages) {
550-
const node = rootDependencies[pkg.name] && rootDependencies[pkg.name].node;
551-
if (!node) {
556+
const node = rootDependencies.get(pkg.name);
557+
if (!node?.package) {
552558
this.logger.error(`Package '${pkg.name}' is not a dependency.`);
553559

554560
return 1;
@@ -627,7 +633,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
627633
return 1;
628634
}
629635

630-
if (manifest.version === node.package.version) {
636+
if (manifest.version === node.package?.version) {
631637
this.logger.info(`Package '${packageName}' is already up to date.`);
632638
continue;
633639
}

packages/angular/cli/lib/init.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'symbol-observable';
1111
import { tags } from '@angular-devkit/core';
1212
import * as fs from 'fs';
1313
import * as path from 'path';
14+
import * as resolve from 'resolve';
1415
import { SemVer } from 'semver';
1516
import { Duplex } from 'stream';
1617
import { colors } from '../utilities/color';
@@ -20,7 +21,7 @@ const packageJson = require('../package.json');
2021

2122
function _fromPackageJson(cwd = process.cwd()): SemVer | null {
2223
do {
23-
const packageJsonPath = path.join(cwd, 'node_modules/@angular/cli/package.json');
24+
const packageJsonPath = resolve.sync('@angular/cli/package.json', { paths: [process.cwd()] });
2425
if (fs.existsSync(packageJsonPath)) {
2526
const content = fs.readFileSync(packageJsonPath, 'utf-8');
2627
if (content) {

packages/angular/cli/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"npm-pick-manifest": "6.1.0",
4040
"open": "7.2.0",
4141
"pacote": "9.5.12",
42-
"read-package-tree": "5.3.1",
42+
"resolve": "1.17.0",
4343
"rimraf": "3.0.2",
4444
"semver": "7.3.2",
4545
"symbol-observable": "1.2.0",
@@ -50,7 +50,7 @@
5050
"migrations": "@schematics/angular/migrations/migration-collection.json",
5151
"packageGroup": {
5252
"@angular/cli": "0.0.0",
53-
"@angular-devkit/build-angular": "0.0.0",
53+
"@angular-devkit/build-angular": "0.0.0",
5454
"@angular-devkit/build-ng-packagr": "0.0.0",
5555
"@angular-devkit/build-webpack": "0.0.0",
5656
"@angular-devkit/core": "0.0.0",

packages/angular/cli/utilities/package-tree.ts

+71-65
Original file line numberDiff line numberDiff line change
@@ -6,88 +6,94 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
9+
import * as fs from 'fs';
10+
import { dirname, join } from 'path';
11+
import * as resolve from 'resolve';
12+
import { promisify } from 'util';
1013
import { NgAddSaveDepedency } from './package-metadata';
1114

12-
export interface PackageTreeNodeBase {
15+
const readFile = promisify(fs.readFile);
16+
17+
interface PackageJson {
1318
name: string;
14-
path: string;
15-
realpath: string;
16-
error?: Error;
17-
id: number;
18-
isLink: boolean;
19-
package: {
20-
name: string;
21-
version: string;
22-
dependencies?: Record<string, string>;
23-
devDependencies?: Record<string, string>;
24-
peerDependencies?: Record<string, string>;
25-
optionalDependencies?: Record<string, string>;
26-
'ng-update'?: {
27-
migrations?: string;
28-
};
29-
'ng-add'?: {
30-
save?: NgAddSaveDepedency;
31-
};
19+
version: string;
20+
dependencies?: Record<string, string>;
21+
devDependencies?: Record<string, string>;
22+
peerDependencies?: Record<string, string>;
23+
optionalDependencies?: Record<string, string>;
24+
'ng-update'?: {
25+
migrations?: string;
26+
};
27+
'ng-add'?: {
28+
save?: NgAddSaveDepedency;
3229
};
33-
parent?: PackageTreeNode;
34-
children: PackageTreeNode[];
3530
}
3631

37-
export interface PackageTreeActual extends PackageTreeNodeBase {
38-
isLink: false;
39-
}
32+
async function readJSON(file: string) {
33+
const buffer = await readFile(file);
4034

41-
export interface PackageTreeLink extends PackageTreeNodeBase {
42-
isLink: true;
43-
target: PackageTreeActual;
35+
return JSON.parse(buffer.toString());
4436
}
4537

46-
export type PackageTreeNode = PackageTreeActual | PackageTreeLink;
38+
function getAllDependencies(pkg: PackageJson) {
39+
return new Set([
40+
...Object.entries(pkg.dependencies || []),
41+
...Object.entries(pkg.devDependencies || []),
42+
...Object.entries(pkg.peerDependencies || []),
43+
...Object.entries(pkg.optionalDependencies || []),
44+
]);
45+
}
4746

48-
export function readPackageTree(path: string): Promise<PackageTreeNode> {
49-
const rpt = require('read-package-tree');
47+
export interface PackageTreeNode {
48+
name: string;
49+
version: string;
50+
path: string;
51+
package: PackageJson | undefined;
52+
}
5053

51-
return new Promise((resolve, reject) => {
52-
rpt(path, (e: Error | undefined, data: PackageTreeNode) => {
53-
if (e) {
54-
reject(e);
55-
} else {
56-
resolve(data);
57-
}
58-
});
59-
});
54+
export async function readPackageJson(packageJsonPath: string): Promise<PackageJson | undefined> {
55+
try {
56+
return await readJSON(packageJsonPath);
57+
} catch (err) {
58+
return undefined;
59+
}
6060
}
6161

62-
export interface NodeDependency {
63-
version: string;
64-
node?: PackageTreeNode;
62+
export function findPackageJson(workspaceDir: string, packageName: string) {
63+
try {
64+
// avoid require.resolve here, see: https://github.com/angular/angular-cli/pull/18610#issuecomment-681980185
65+
const packageJsonPath = resolve.sync(`${packageName}/package.json`, { paths: [workspaceDir] });
66+
67+
return packageJsonPath;
68+
} catch (err) {
69+
return undefined;
70+
}
6571
}
6672

67-
export function findNodeDependencies(node: PackageTreeNode) {
68-
const rawDeps: Record<string, string> = {
69-
...node.package.dependencies,
70-
...node.package.devDependencies,
71-
...node.package.peerDependencies,
72-
...node.package.optionalDependencies,
73-
};
73+
export async function getProjectDependencies(dir: string) {
74+
const pkgJsonPath = resolve.sync(join(dir, `package.json`));
75+
if (!pkgJsonPath) {
76+
throw new Error('Could not find package.json');
77+
}
7478

75-
return Object.entries(rawDeps).reduce(
76-
(deps, [name, version]) => {
77-
let dependencyNode;
78-
let parent: PackageTreeNode | undefined | null = node;
79-
while (!dependencyNode && parent) {
80-
dependencyNode = parent.children.find(child => child.name === name);
81-
parent = parent.parent;
82-
}
79+
const pkg: PackageJson = await readJSON(pkgJsonPath);
8380

84-
deps[name] = {
85-
node: dependencyNode,
86-
version,
87-
};
81+
const results = new Map<string, PackageTreeNode>();
82+
await Promise.all(
83+
Array.from(getAllDependencies(pkg)).map(async ([name, version]) => {
84+
const packageJsonPath = findPackageJson(dir, name);
85+
if (packageJsonPath) {
86+
const currentDependency = {
87+
name,
88+
version,
89+
path: dirname(packageJsonPath),
90+
package: await readPackageJson(packageJsonPath),
91+
};
8892

89-
return deps;
90-
},
91-
Object.create(null) as Record<string, NodeDependency>,
93+
results.set(currentDependency.name, currentDependency);
94+
}
95+
}),
9296
);
97+
98+
return results;
9399
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"private": true,
3+
"workspaces": {
4+
"packages": [
5+
"packages/*"
6+
],
7+
"nohoist": [
8+
"**/codelyzer",
9+
"**/codelyzer/**",
10+
"**/@angular*",
11+
"**/@angular*/**"
12+
]
13+
}
14+
}

0 commit comments

Comments
 (0)