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-
98// tslint:disable:no-global-tslint-disable no-any
109import { tags , terminal } from '@angular-devkit/core' ;
10+ import { ModuleNotFoundException , resolve } from '@angular-devkit/core/node' ;
1111import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools' ;
12+ import { dirname } from 'path' ;
13+ import { intersects , prerelease , rcompare , satisfies , valid , validRange } from 'semver' ;
1214import { parseOptions } from '../models/command-runner' ;
1315import { SchematicCommand } from '../models/schematic-command' ;
1416import { NpmInstall } from '../tasks/npm-install' ;
1517import { getPackageManager } from '../utilities/config' ;
18+ import {
19+ PackageManifest ,
20+ fetchPackageManifest ,
21+ fetchPackageMetadata ,
22+ } from '../utilities/package-metadata' ;
1623
24+ const npa = require ( 'npm-package-arg' ) ;
1725
1826export class AddCommand extends SchematicCommand {
1927 readonly allowPrivateSchematics = true ;
28+ readonly packageManager = getPackageManager ( ) ;
2029
2130 private async _parseSchematicOptions ( collectionName : string ) : Promise < any > {
2231 const schematicOptions = await this . getOptions ( {
@@ -55,35 +64,137 @@ export class AddCommand extends SchematicCommand {
5564 return 1 ;
5665 }
5766
58- const packageManager = getPackageManager ( ) ;
67+ let packageIdentifier ;
68+ try {
69+ packageIdentifier = npa ( options . collection ) ;
70+ } catch ( e ) {
71+ this . logger . error ( e . message ) ;
5972
60- const npmInstall : NpmInstall = require ( '../tasks/npm-install' ) . default ;
73+ return 1 ;
74+ }
75+
76+ if ( packageIdentifier . registry && this . isPackageInstalled ( packageIdentifier . name ) ) {
77+ // Already installed so just run schematic
78+ this . logger . info ( 'Skipping installation: Package already installed' ) ;
79+
80+ // Reparse the options with the new schematic accessible.
81+ options = await this . _parseSchematicOptions ( packageIdentifier . name ) ;
82+
83+ return this . executeSchematic ( packageIdentifier . name , options ) ;
84+ }
85+
86+ const usingYarn = this . packageManager === 'yarn' ;
87+
88+ if ( packageIdentifier . type === 'tag' && ! packageIdentifier . rawSpec ) {
89+ // only package name provided; search for viable version
90+ // plus special cases for packages that did not have peer deps setup
91+ let packageMetadata ;
92+ try {
93+ packageMetadata = await fetchPackageMetadata (
94+ packageIdentifier . name ,
95+ this . logger ,
96+ { usingYarn } ,
97+ ) ;
98+ } catch ( e ) {
99+ this . logger . error ( 'Unable to fetch package metadata: ' + e . message ) ;
100+
101+ return 1 ;
102+ }
103+
104+ const latestManifest = packageMetadata . tags [ 'latest' ] ;
105+ if ( latestManifest && Object . keys ( latestManifest . peerDependencies ) . length === 0 ) {
106+ if ( latestManifest . name === '@angular/pwa' ) {
107+ const version = await this . findProjectVersion ( '@angular/cli' ) ;
108+ // tslint:disable-next-line:no-any
109+ const semverOptions = { includePrerelease : true } as any ;
61110
62- const packageName = firstArg . startsWith ( '@' )
63- ? firstArg . split ( '/' , 2 ) . join ( '/' )
64- : firstArg . split ( '/' , 1 ) [ 0 ] ;
111+ if ( version
112+ && ( ( validRange ( version ) && intersects ( version , '6' , semverOptions ) )
113+ || ( valid ( version ) && satisfies ( version , '6' , semverOptions ) ) ) ) {
114+ packageIdentifier = npa . resolve ( '@angular/pwa' , 'v6-lts' ) ;
115+ }
116+ }
117+ } else if ( ! latestManifest || ( await this . hasMismatchedPeer ( latestManifest ) ) ) {
118+ // 'latest' is invalid so search for most recent matching package
119+ const versionManifests = Array . from ( packageMetadata . versions . values ( ) )
120+ . filter ( value => ! prerelease ( value . version ) ) ;
65121
66- // Remove the tag/version from the package name.
67- const collectionName = (
68- packageName . startsWith ( '@' )
69- ? packageName . split ( '@' , 2 ) . join ( '@' )
70- : packageName . split ( '@' , 1 ) . join ( '@' )
71- ) + firstArg . slice ( packageName . length ) ;
122+ versionManifests . sort ( ( a , b ) => rcompare ( a . version , b . version , true ) ) ;
123+
124+ let newIdentifier ;
125+ for ( const versionManifest of versionManifests ) {
126+ if ( ! ( await this . hasMismatchedPeer ( versionManifest ) ) ) {
127+ newIdentifier = npa . resolve ( packageIdentifier . name , versionManifest . version ) ;
128+ break ;
129+ }
130+ }
131+
132+ if ( ! newIdentifier ) {
133+ this . logger . warn ( 'Unable to find compatible package. Using \'latest\'.' ) ;
134+ } else {
135+ packageIdentifier = newIdentifier ;
136+ }
137+ }
138+ }
139+
140+ let collectionName = packageIdentifier . name ;
141+ if ( ! packageIdentifier . registry ) {
142+ try {
143+ const manifest = await fetchPackageManifest (
144+ packageIdentifier ,
145+ this . logger ,
146+ { usingYarn } ,
147+ ) ;
148+
149+ collectionName = manifest . name ;
150+
151+ if ( await this . hasMismatchedPeer ( manifest ) ) {
152+ console . warn ( 'Package has unmet peer dependencies. Adding the package may not succeed.' ) ;
153+ }
154+ } catch ( e ) {
155+ this . logger . error ( 'Unable to fetch package manifest: ' + e . message ) ;
156+
157+ return 1 ;
158+ }
159+ }
160+
161+ const npmInstall : NpmInstall = require ( '../tasks/npm-install' ) . default ;
72162
73163 // We don't actually add the package to package.json, that would be the work of the package
74164 // itself.
75165 await npmInstall (
76- packageName ,
166+ packageIdentifier . raw ,
77167 this . logger ,
78- packageManager ,
168+ this . packageManager ,
79169 this . project . root ,
80170 ) ;
81171
82172 // Reparse the options with the new schematic accessible.
83173 options = await this . _parseSchematicOptions ( collectionName ) ;
84174
175+ return this . executeSchematic ( collectionName , options ) ;
176+ }
177+
178+ private isPackageInstalled ( name : string ) : boolean {
179+ try {
180+ resolve ( name , { checkLocal : true , basedir : this . project . root } ) ;
181+
182+ return true ;
183+ } catch ( e ) {
184+ if ( ! ( e instanceof ModuleNotFoundException ) ) {
185+ throw e ;
186+ }
187+ }
188+
189+ return false ;
190+ }
191+
192+ private async executeSchematic (
193+ collectionName : string ,
194+ options ?: string [ ] ,
195+ ) : Promise < number | void > {
85196 const runOptions = {
86- schematicOptions : options ,
197+ schematicOptions : options || [ ] ,
87198 workingDir : this . project . root ,
88199 collectionName,
89200 schematicName : 'ng-add' ,
@@ -107,4 +218,79 @@ export class AddCommand extends SchematicCommand {
107218 throw e ;
108219 }
109220 }
221+
222+ private async findProjectVersion ( name : string ) : Promise < string | null > {
223+ let installedPackage ;
224+ try {
225+ installedPackage = resolve (
226+ name ,
227+ { checkLocal : true , basedir : this . project . root , resolvePackageJson : true } ,
228+ ) ;
229+ } catch { }
230+
231+ if ( installedPackage ) {
232+ try {
233+ const installed = await fetchPackageManifest ( dirname ( installedPackage ) , this . logger ) ;
234+
235+ return installed . version ;
236+ } catch { }
237+ }
238+
239+ let projectManifest ;
240+ try {
241+ projectManifest = await fetchPackageManifest ( this . project . root , this . logger ) ;
242+ } catch { }
243+
244+ if ( projectManifest ) {
245+ let version = projectManifest . dependencies [ name ] ;
246+ if ( version ) {
247+ return version ;
248+ }
249+
250+ version = projectManifest . devDependencies [ name ] ;
251+ if ( version ) {
252+ return version ;
253+ }
254+ }
255+
256+ return null ;
257+ }
258+
259+ private async hasMismatchedPeer ( manifest : PackageManifest ) : Promise < boolean > {
260+ for ( const peer in manifest . peerDependencies ) {
261+ let peerIdentifier ;
262+ try {
263+ peerIdentifier = npa . resolve ( peer , manifest . peerDependencies [ peer ] ) ;
264+ } catch {
265+ this . logger . warn ( `Invalid peer dependency ${ peer } found in package.` ) ;
266+ continue ;
267+ }
268+
269+ if ( peerIdentifier . type === 'version' || peerIdentifier . type === 'range' ) {
270+ try {
271+ const version = await this . findProjectVersion ( peer ) ;
272+ if ( ! version ) {
273+ continue ;
274+ }
275+
276+ // tslint:disable-next-line:no-any
277+ const options = { includePrerelease : true } as any ;
278+
279+ if ( ! intersects ( version , peerIdentifier . rawSpec , options )
280+ && ! satisfies ( version , peerIdentifier . rawSpec , options ) ) {
281+ return true ;
282+ }
283+ } catch {
284+ // Not found or invalid so ignore
285+ continue ;
286+ }
287+ } else {
288+ // type === 'tag' | 'file' | 'directory' | 'remote' | 'git'
289+ // Cannot accurately compare these as the tag/location may have changed since install
290+ }
291+
292+ }
293+
294+ return false ;
295+ }
110296}
0 commit comments