55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.io/license
77 */
8-
9- // tslint:disable:no-global-tslint-disable no-any
108import { tags , terminal } from '@angular-devkit/core' ;
9+ import { ModuleNotFoundException , resolve } from '@angular-devkit/core/node' ;
1110import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools' ;
11+ import { dirname } from 'path' ;
12+ import { intersects , prerelease , rcompare , satisfies , valid , validRange } from 'semver' ;
1213import { Arguments } from '../models/interface' ;
1314import { SchematicCommand } from '../models/schematic-command' ;
14- import { NpmInstall } from '../tasks/npm-install' ;
15+ import npmInstall from '../tasks/npm-install' ;
1516import { getPackageManager } from '../utilities/package-manager' ;
17+ import {
18+ PackageManifest ,
19+ fetchPackageManifest ,
20+ fetchPackageMetadata ,
21+ } from '../utilities/package-metadata' ;
1622import { Schema as AddCommandSchema } from './add' ;
1723
24+ const npa = require ( 'npm-package-arg' ) ;
25+
1826export class AddCommand extends SchematicCommand < AddCommandSchema > {
1927 readonly allowPrivateSchematics = true ;
28+ readonly packageManager = getPackageManager ( this . workspace . root ) ;
2029
2130 async run ( options : AddCommandSchema & Arguments ) {
2231 if ( ! options . collection ) {
@@ -28,32 +37,127 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
2837 return 1 ;
2938 }
3039
31- const packageManager = getPackageManager ( this . workspace . root ) ;
40+ let packageIdentifier ;
41+ try {
42+ packageIdentifier = npa ( options . collection ) ;
43+ } catch ( e ) {
44+ this . logger . error ( e . message ) ;
45+
46+ return 1 ;
47+ }
48+
49+ if ( packageIdentifier . registry && this . isPackageInstalled ( packageIdentifier . name ) ) {
50+ // Already installed so just run schematic
51+ this . logger . info ( 'Skipping installation: Package already installed' ) ;
52+
53+ return this . executeSchematic ( packageIdentifier . name , options [ '--' ] ) ;
54+ }
55+
56+ const usingYarn = this . packageManager === 'yarn' ;
57+
58+ if ( packageIdentifier . type === 'tag' && ! packageIdentifier . rawSpec ) {
59+ // only package name provided; search for viable version
60+ // plus special cases for packages that did not have peer deps setup
61+ let packageMetadata ;
62+ try {
63+ packageMetadata = await fetchPackageMetadata (
64+ packageIdentifier . name ,
65+ this . logger ,
66+ { usingYarn } ,
67+ ) ;
68+ } catch ( e ) {
69+ this . logger . error ( 'Unable to fetch package metadata: ' + e . message ) ;
70+
71+ return 1 ;
72+ }
73+
74+ const latestManifest = packageMetadata . tags [ 'latest' ] ;
75+ if ( latestManifest && Object . keys ( latestManifest . peerDependencies ) . length === 0 ) {
76+ if ( latestManifest . name === '@angular/pwa' ) {
77+ const version = await this . findProjectVersion ( '@angular/cli' ) ;
78+ // tslint:disable-next-line:no-any
79+ const semverOptions = { includePrerelease : true } as any ;
80+
81+ if ( version
82+ && ( ( validRange ( version ) && intersects ( version , '7' , semverOptions ) )
83+ || ( valid ( version ) && satisfies ( version , '7' , semverOptions ) ) ) ) {
84+ packageIdentifier = npa . resolve ( '@angular/pwa' , '0.12' ) ;
85+ }
86+ }
87+ } else if ( ! latestManifest || ( await this . hasMismatchedPeer ( latestManifest ) ) ) {
88+ // 'latest' is invalid so search for most recent matching package
89+ const versionManifests = Array . from ( packageMetadata . versions . values ( ) )
90+ . filter ( value => ! prerelease ( value . version ) ) ;
91+
92+ versionManifests . sort ( ( a , b ) => rcompare ( a . version , b . version , true ) ) ;
93+
94+ let newIdentifier ;
95+ for ( const versionManifest of versionManifests ) {
96+ if ( ! ( await this . hasMismatchedPeer ( versionManifest ) ) ) {
97+ newIdentifier = npa . resolve ( packageIdentifier . name , versionManifest . version ) ;
98+ break ;
99+ }
100+ }
101+
102+ if ( ! newIdentifier ) {
103+ this . logger . warn ( 'Unable to find compatible package. Using \'latest\'.' ) ;
104+ } else {
105+ packageIdentifier = newIdentifier ;
106+ }
107+ }
108+ }
109+
110+ let collectionName = packageIdentifier . name ;
111+ if ( ! packageIdentifier . registry ) {
112+ try {
113+ const manifest = await fetchPackageManifest (
114+ packageIdentifier ,
115+ this . logger ,
116+ { usingYarn } ,
117+ ) ;
32118
33- const npmInstall : NpmInstall = require ( '../tasks/npm-install' ) . default ;
119+ collectionName = manifest . name ;
34120
35- const packageName = options . collection . startsWith ( '@' )
36- ? options . collection . split ( '/' , 2 ) . join ( '/' )
37- : options . collection . split ( '/' , 1 ) [ 0 ] ;
121+ if ( await this . hasMismatchedPeer ( manifest ) ) {
122+ console . warn ( 'Package has unmet peer dependencies. Adding the package may not succeed.' ) ;
123+ }
124+ } catch ( e ) {
125+ this . logger . error ( 'Unable to fetch package manifest: ' + e . message ) ;
38126
39- // Remove the tag/version from the package name.
40- const collectionName = (
41- packageName . startsWith ( '@' )
42- ? packageName . split ( '@' , 2 ) . join ( '@' )
43- : packageName . split ( '@' , 1 ) . join ( '@' )
44- ) + options . collection . slice ( packageName . length ) ;
127+ return 1 ;
128+ }
129+ }
45130
46- // We don't actually add the package to package.json, that would be the work of the package
47- // itself.
48131 await npmInstall (
49- packageName ,
132+ packageIdentifier . raw ,
50133 this . logger ,
51- packageManager ,
134+ this . packageManager ,
52135 this . workspace . root ,
53136 ) ;
54137
138+ return this . executeSchematic ( collectionName , options [ '--' ] ) ;
139+ }
140+
141+ private isPackageInstalled ( name : string ) : boolean {
142+ try {
143+ resolve ( name , { checkLocal : true , basedir : this . workspace . root } ) ;
144+
145+ return true ;
146+ } catch ( e ) {
147+ if ( ! ( e instanceof ModuleNotFoundException ) ) {
148+ throw e ;
149+ }
150+ }
151+
152+ return false ;
153+ }
154+
155+ private async executeSchematic (
156+ collectionName : string ,
157+ options : string [ ] = [ ] ,
158+ ) : Promise < number | void > {
55159 const runOptions = {
56- schematicOptions : options [ '--' ] || [ ] ,
160+ schematicOptions : options ,
57161 workingDir : this . workspace . root ,
58162 collectionName,
59163 schematicName : 'ng-add' ,
@@ -77,4 +181,74 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
77181 throw e ;
78182 }
79183 }
184+
185+ private async findProjectVersion ( name : string ) : Promise < string | null > {
186+ let installedPackage ;
187+ try {
188+ installedPackage = resolve (
189+ name ,
190+ { checkLocal : true , basedir : this . workspace . root , resolvePackageJson : true } ,
191+ ) ;
192+ } catch { }
193+
194+ if ( installedPackage ) {
195+ try {
196+ const installed = await fetchPackageManifest ( dirname ( installedPackage ) , this . logger ) ;
197+
198+ return installed . version ;
199+ } catch { }
200+ }
201+
202+ let projectManifest ;
203+ try {
204+ projectManifest = await fetchPackageManifest ( this . workspace . root , this . logger ) ;
205+ } catch { }
206+
207+ if ( projectManifest ) {
208+ const version = projectManifest . dependencies [ name ] || projectManifest . devDependencies [ name ] ;
209+ if ( version ) {
210+ return version ;
211+ }
212+ }
213+
214+ return null ;
215+ }
216+
217+ private async hasMismatchedPeer ( manifest : PackageManifest ) : Promise < boolean > {
218+ for ( const peer in manifest . peerDependencies ) {
219+ let peerIdentifier ;
220+ try {
221+ peerIdentifier = npa . resolve ( peer , manifest . peerDependencies [ peer ] ) ;
222+ } catch {
223+ this . logger . warn ( `Invalid peer dependency ${ peer } found in package.` ) ;
224+ continue ;
225+ }
226+
227+ if ( peerIdentifier . type === 'version' || peerIdentifier . type === 'range' ) {
228+ try {
229+ const version = await this . findProjectVersion ( peer ) ;
230+ if ( ! version ) {
231+ continue ;
232+ }
233+
234+ // tslint:disable-next-line:no-any
235+ const options = { includePrerelease : true } as any ;
236+
237+ if ( ! intersects ( version , peerIdentifier . rawSpec , options )
238+ && ! satisfies ( version , peerIdentifier . rawSpec , options ) ) {
239+ return true ;
240+ }
241+ } catch {
242+ // Not found or invalid so ignore
243+ continue ;
244+ }
245+ } else {
246+ // type === 'tag' | 'file' | 'directory' | 'remote' | 'git'
247+ // Cannot accurately compare these as the tag/location may have changed since install
248+ }
249+
250+ }
251+
252+ return false ;
253+ }
80254}
0 commit comments