5
5
* Use of this source code is governed by an MIT-style license that can be
6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
-
9
- // tslint:disable:no-global-tslint-disable no-any
10
8
import { tags , terminal } from '@angular-devkit/core' ;
9
+ import { ModuleNotFoundException , resolve } from '@angular-devkit/core/node' ;
11
10
import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools' ;
11
+ import { dirname } from 'path' ;
12
+ import { intersects , prerelease , rcompare , satisfies , valid , validRange } from 'semver' ;
12
13
import { Arguments } from '../models/interface' ;
13
14
import { SchematicCommand } from '../models/schematic-command' ;
14
- import { NpmInstall } from '../tasks/npm-install' ;
15
+ import npmInstall from '../tasks/npm-install' ;
15
16
import { getPackageManager } from '../utilities/package-manager' ;
17
+ import {
18
+ PackageManifest ,
19
+ fetchPackageManifest ,
20
+ fetchPackageMetadata ,
21
+ } from '../utilities/package-metadata' ;
16
22
import { Schema as AddCommandSchema } from './add' ;
17
23
24
+ const npa = require ( 'npm-package-arg' ) ;
25
+
18
26
export class AddCommand extends SchematicCommand < AddCommandSchema > {
19
27
readonly allowPrivateSchematics = true ;
28
+ readonly packageManager = getPackageManager ( this . workspace . root ) ;
20
29
21
30
async run ( options : AddCommandSchema & Arguments ) {
22
31
if ( ! options . collection ) {
@@ -28,32 +37,127 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
28
37
return 1 ;
29
38
}
30
39
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
+ ) ;
32
118
33
- const npmInstall : NpmInstall = require ( '../tasks/npm-install' ) . default ;
119
+ collectionName = manifest . name ;
34
120
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 ) ;
38
126
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
+ }
45
130
46
- // We don't actually add the package to package.json, that would be the work of the package
47
- // itself.
48
131
await npmInstall (
49
- packageName ,
132
+ packageIdentifier . raw ,
50
133
this . logger ,
51
- packageManager ,
134
+ this . packageManager ,
52
135
this . workspace . root ,
53
136
) ;
54
137
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 > {
55
159
const runOptions = {
56
- schematicOptions : options [ '--' ] || [ ] ,
160
+ schematicOptions : options ,
57
161
workingDir : this . workspace . root ,
58
162
collectionName,
59
163
schematicName : 'ng-add' ,
@@ -77,4 +181,74 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
77
181
throw e ;
78
182
}
79
183
}
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
+ }
80
254
}
0 commit comments