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
8
// tslint:disable:no-global-tslint-disable no-any
10
9
import { tags , terminal } from '@angular-devkit/core' ;
10
+ import { ModuleNotFoundException , resolve } from '@angular-devkit/core/node' ;
11
11
import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools' ;
12
+ import { dirname } from 'path' ;
13
+ import { intersects , prerelease , rcompare , satisfies , valid , validRange } from 'semver' ;
12
14
import { parseOptions } from '../models/command-runner' ;
13
15
import { SchematicCommand } from '../models/schematic-command' ;
14
16
import { NpmInstall } from '../tasks/npm-install' ;
15
17
import { getPackageManager } from '../utilities/config' ;
18
+ import {
19
+ PackageManifest ,
20
+ fetchPackageManifest ,
21
+ fetchPackageMetadata ,
22
+ } from '../utilities/package-metadata' ;
16
23
24
+ const npa = require ( 'npm-package-arg' ) ;
17
25
18
26
export class AddCommand extends SchematicCommand {
19
27
readonly allowPrivateSchematics = true ;
28
+ readonly packageManager = getPackageManager ( ) ;
20
29
21
30
private async _parseSchematicOptions ( collectionName : string ) : Promise < any > {
22
31
const schematicOptions = await this . getOptions ( {
@@ -55,35 +64,137 @@ export class AddCommand extends SchematicCommand {
55
64
return 1 ;
56
65
}
57
66
58
- const packageManager = getPackageManager ( ) ;
67
+ let packageIdentifier ;
68
+ try {
69
+ packageIdentifier = npa ( options . collection ) ;
70
+ } catch ( e ) {
71
+ this . logger . error ( e . message ) ;
59
72
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 ;
61
110
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 ) ) ;
65
121
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 ;
72
162
73
163
// We don't actually add the package to package.json, that would be the work of the package
74
164
// itself.
75
165
await npmInstall (
76
- packageName ,
166
+ packageIdentifier . raw ,
77
167
this . logger ,
78
- packageManager ,
168
+ this . packageManager ,
79
169
this . project . root ,
80
170
) ;
81
171
82
172
// Reparse the options with the new schematic accessible.
83
173
options = await this . _parseSchematicOptions ( collectionName ) ;
84
174
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 > {
85
196
const runOptions = {
86
- schematicOptions : options ,
197
+ schematicOptions : options || [ ] ,
87
198
workingDir : this . project . root ,
88
199
collectionName,
89
200
schematicName : 'ng-add' ,
@@ -107,4 +218,79 @@ export class AddCommand extends SchematicCommand {
107
218
throw e ;
108
219
}
109
220
}
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
+ }
110
296
}
0 commit comments