mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-18 11:44:05 +08:00
ng-add checks if a specified collection is installed, and if not it'd proceed to install the package. However, `isPackageInstalled` would, by default, resolve the main field or the index of the package. Not all NPM packages specify the main field or provide an index file. It should be sufficient to just check the presence of `package.json` to detect whether a package is installed or not. For example, `ng add @angular/bazel` should not install the package if it's already installed locally. `@angular/bazel` does not specify a main field not an index file in its `package.json`.
259 lines
7.8 KiB
TypeScript
259 lines
7.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
import { tags, terminal } from '@angular-devkit/core';
|
|
import { ModuleNotFoundException, resolve } from '@angular-devkit/core/node';
|
|
import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools';
|
|
import { dirname } from 'path';
|
|
import { intersects, prerelease, rcompare, satisfies, valid, validRange } from 'semver';
|
|
import { Arguments } from '../models/interface';
|
|
import { SchematicCommand } from '../models/schematic-command';
|
|
import npmInstall from '../tasks/npm-install';
|
|
import { getPackageManager } from '../utilities/package-manager';
|
|
import {
|
|
PackageManifest,
|
|
fetchPackageManifest,
|
|
fetchPackageMetadata,
|
|
} from '../utilities/package-metadata';
|
|
import { Schema as AddCommandSchema } from './add';
|
|
|
|
const npa = require('npm-package-arg');
|
|
|
|
export class AddCommand extends SchematicCommand<AddCommandSchema> {
|
|
readonly allowPrivateSchematics = true;
|
|
readonly packageManager = getPackageManager(this.workspace.root);
|
|
|
|
async run(options: AddCommandSchema & Arguments) {
|
|
if (!options.collection) {
|
|
this.logger.fatal(
|
|
`The "ng add" command requires a name argument to be specified eg. `
|
|
+ `${terminal.yellow('ng add [name] ')}. For more details, use "ng help".`,
|
|
);
|
|
|
|
return 1;
|
|
}
|
|
|
|
let packageIdentifier;
|
|
try {
|
|
packageIdentifier = npa(options.collection);
|
|
} catch (e) {
|
|
this.logger.error(e.message);
|
|
|
|
return 1;
|
|
}
|
|
|
|
if (packageIdentifier.registry && this.isPackageInstalled(packageIdentifier.name)) {
|
|
// Already installed so just run schematic
|
|
this.logger.info('Skipping installation: Package already installed');
|
|
|
|
return this.executeSchematic(packageIdentifier.name, options['--']);
|
|
}
|
|
|
|
const usingYarn = this.packageManager === 'yarn';
|
|
|
|
if (packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) {
|
|
// only package name provided; search for viable version
|
|
// plus special cases for packages that did not have peer deps setup
|
|
let packageMetadata;
|
|
try {
|
|
packageMetadata = await fetchPackageMetadata(
|
|
packageIdentifier.name,
|
|
this.logger,
|
|
{ usingYarn },
|
|
);
|
|
} catch (e) {
|
|
this.logger.error('Unable to fetch package metadata: ' + e.message);
|
|
|
|
return 1;
|
|
}
|
|
|
|
const latestManifest = packageMetadata.tags['latest'];
|
|
if (latestManifest && Object.keys(latestManifest.peerDependencies).length === 0) {
|
|
if (latestManifest.name === '@angular/pwa') {
|
|
const version = await this.findProjectVersion('@angular/cli');
|
|
// tslint:disable-next-line:no-any
|
|
const semverOptions = { includePrerelease: true } as any;
|
|
|
|
if (version
|
|
&& ((validRange(version) && intersects(version, '7', semverOptions))
|
|
|| (valid(version) && satisfies(version, '7', semverOptions)))) {
|
|
packageIdentifier = npa.resolve('@angular/pwa', '0.12');
|
|
}
|
|
}
|
|
} else if (!latestManifest || (await this.hasMismatchedPeer(latestManifest))) {
|
|
// 'latest' is invalid so search for most recent matching package
|
|
const versionManifests = Array.from(packageMetadata.versions.values())
|
|
.filter(value => !prerelease(value.version));
|
|
|
|
versionManifests.sort((a, b) => rcompare(a.version, b.version, true));
|
|
|
|
let newIdentifier;
|
|
for (const versionManifest of versionManifests) {
|
|
if (!(await this.hasMismatchedPeer(versionManifest))) {
|
|
newIdentifier = npa.resolve(packageIdentifier.name, versionManifest.version);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!newIdentifier) {
|
|
this.logger.warn('Unable to find compatible package. Using \'latest\'.');
|
|
} else {
|
|
packageIdentifier = newIdentifier;
|
|
}
|
|
}
|
|
}
|
|
|
|
let collectionName = packageIdentifier.name;
|
|
if (!packageIdentifier.registry) {
|
|
try {
|
|
const manifest = await fetchPackageManifest(
|
|
packageIdentifier,
|
|
this.logger,
|
|
{ usingYarn },
|
|
);
|
|
|
|
collectionName = manifest.name;
|
|
|
|
if (await this.hasMismatchedPeer(manifest)) {
|
|
console.warn('Package has unmet peer dependencies. Adding the package may not succeed.');
|
|
}
|
|
} catch (e) {
|
|
this.logger.error('Unable to fetch package manifest: ' + e.message);
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
await npmInstall(
|
|
packageIdentifier.raw,
|
|
this.logger,
|
|
this.packageManager,
|
|
this.workspace.root,
|
|
);
|
|
|
|
return this.executeSchematic(collectionName, options['--']);
|
|
}
|
|
|
|
private isPackageInstalled(name: string): boolean {
|
|
try {
|
|
resolve(name, {
|
|
checkLocal: true,
|
|
basedir: this.workspace.root,
|
|
resolvePackageJson: true,
|
|
});
|
|
|
|
return true;
|
|
} catch (e) {
|
|
if (!(e instanceof ModuleNotFoundException)) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async executeSchematic(
|
|
collectionName: string,
|
|
options: string[] = [],
|
|
): Promise<number | void> {
|
|
const runOptions = {
|
|
schematicOptions: options,
|
|
workingDir: this.workspace.root,
|
|
collectionName,
|
|
schematicName: 'ng-add',
|
|
allowPrivate: true,
|
|
dryRun: false,
|
|
force: false,
|
|
};
|
|
|
|
try {
|
|
return await this.runSchematic(runOptions);
|
|
} catch (e) {
|
|
if (e instanceof NodePackageDoesNotSupportSchematics) {
|
|
this.logger.error(tags.oneLine`
|
|
The package that you are trying to add does not support schematics. You can try using
|
|
a different version of the package or contact the package author to add ng-add support.
|
|
`);
|
|
|
|
return 1;
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private async findProjectVersion(name: string): Promise<string | null> {
|
|
let installedPackage;
|
|
try {
|
|
installedPackage = resolve(
|
|
name,
|
|
{ checkLocal: true, basedir: this.workspace.root, resolvePackageJson: true },
|
|
);
|
|
} catch { }
|
|
|
|
if (installedPackage) {
|
|
try {
|
|
const installed = await fetchPackageManifest(dirname(installedPackage), this.logger);
|
|
|
|
return installed.version;
|
|
} catch {}
|
|
}
|
|
|
|
let projectManifest;
|
|
try {
|
|
projectManifest = await fetchPackageManifest(this.workspace.root, this.logger);
|
|
} catch {}
|
|
|
|
if (projectManifest) {
|
|
const version = projectManifest.dependencies[name] || projectManifest.devDependencies[name];
|
|
if (version) {
|
|
return version;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async hasMismatchedPeer(manifest: PackageManifest): Promise<boolean> {
|
|
for (const peer in manifest.peerDependencies) {
|
|
let peerIdentifier;
|
|
try {
|
|
peerIdentifier = npa.resolve(peer, manifest.peerDependencies[peer]);
|
|
} catch {
|
|
this.logger.warn(`Invalid peer dependency ${peer} found in package.`);
|
|
continue;
|
|
}
|
|
|
|
if (peerIdentifier.type === 'version' || peerIdentifier.type === 'range') {
|
|
try {
|
|
const version = await this.findProjectVersion(peer);
|
|
if (!version) {
|
|
continue;
|
|
}
|
|
|
|
// tslint:disable-next-line:no-any
|
|
const options = { includePrerelease: true } as any;
|
|
|
|
if (!intersects(version, peerIdentifier.rawSpec, options)
|
|
&& !satisfies(version, peerIdentifier.rawSpec, options)) {
|
|
return true;
|
|
}
|
|
} catch {
|
|
// Not found or invalid so ignore
|
|
continue;
|
|
}
|
|
} else {
|
|
// type === 'tag' | 'file' | 'directory' | 'remote' | 'git'
|
|
// Cannot accurately compare these as the tag/location may have changed since install
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|