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
- import { JsonObject , Path , join , normalize , tags } from '@angular-devkit/core' ;
8
+ import {
9
+ JsonObject ,
10
+ JsonParseMode ,
11
+ Path ,
12
+ join ,
13
+ normalize ,
14
+ parseJson ,
15
+ parseJsonAst ,
16
+ tags ,
17
+ } from '@angular-devkit/core' ;
9
18
import {
10
19
Rule ,
11
20
SchematicContext ,
@@ -16,6 +25,11 @@ import {
16
25
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks' ;
17
26
import { AppConfig , CliConfig } from '../../utility/config' ;
18
27
import { latestVersions } from '../../utility/latest-versions' ;
28
+ import {
29
+ appendPropertyInAstObject ,
30
+ appendValueInAstArray ,
31
+ findPropertyInAstObject ,
32
+ } from './json-utils' ;
19
33
20
34
const defaults = {
21
35
appRoot : 'src' ,
@@ -492,8 +506,6 @@ function extractProjectsConfig(config: CliConfig, tree: Tree): JsonObject {
492
506
root : project . root ,
493
507
sourceRoot : project . root ,
494
508
projectType : 'application' ,
495
- cli : { } ,
496
- schematics : { } ,
497
509
} ;
498
510
499
511
const e2eArchitect : JsonObject = { } ;
@@ -542,51 +554,91 @@ function updateSpecTsConfig(config: CliConfig): Rule {
542
554
return ( host : Tree , context : SchematicContext ) => {
543
555
const apps = config . apps || [ ] ;
544
556
apps . forEach ( ( app : AppConfig , idx : number ) => {
545
- const tsSpecConfigPath =
546
- join ( app . root as Path , app . testTsconfig || defaults . testTsConfig ) ;
557
+ const testTsConfig = app . testTsconfig || defaults . testTsConfig ;
558
+ const tsSpecConfigPath = join ( normalize ( app . root || '' ) , testTsConfig ) ;
547
559
const buffer = host . read ( tsSpecConfigPath ) ;
560
+
548
561
if ( ! buffer ) {
549
562
return ;
550
563
}
551
- const tsCfg = JSON . parse ( buffer . toString ( ) ) ;
552
- if ( ! tsCfg . files ) {
553
- tsCfg . files = [ ] ;
564
+
565
+
566
+ const tsCfgAst = parseJsonAst ( buffer . toString ( ) , JsonParseMode . Loose ) ;
567
+ if ( tsCfgAst . kind != 'object' ) {
568
+ throw new SchematicsException ( 'Invalid tsconfig. Was expecting an object' ) ;
554
569
}
555
570
556
- // Ensure the spec tsconfig contains the polyfills file
557
- if ( tsCfg . files . indexOf ( app . polyfills || defaults . polyfills ) === - 1 ) {
558
- tsCfg . files . push ( app . polyfills || defaults . polyfills ) ;
559
- host . overwrite ( tsSpecConfigPath , JSON . stringify ( tsCfg , null , 2 ) ) ;
571
+ const filesAstNode = findPropertyInAstObject ( tsCfgAst , 'files' ) ;
572
+ if ( filesAstNode && filesAstNode . kind != 'array' ) {
573
+ throw new SchematicsException ( 'Invalid tsconfig "files" property; expected an array.' ) ;
560
574
}
575
+
576
+ const recorder = host . beginUpdate ( tsSpecConfigPath ) ;
577
+
578
+ const polyfills = app . polyfills || defaults . polyfills ;
579
+ if ( ! filesAstNode ) {
580
+ // Do nothing if the files array does not exist. This means exclude or include are
581
+ // set and we shouldn't mess with that.
582
+ } else {
583
+ if ( filesAstNode . value . indexOf ( polyfills ) == - 1 ) {
584
+ appendValueInAstArray ( recorder , filesAstNode , polyfills ) ;
585
+ }
586
+ }
587
+
588
+ host . commitUpdate ( recorder ) ;
561
589
} ) ;
562
590
} ;
563
591
}
564
592
565
- function updatePackageJson ( packageManager ?: string ) {
593
+ function updatePackageJson ( config : CliConfig ) {
566
594
return ( host : Tree , context : SchematicContext ) => {
567
595
const pkgPath = '/package.json' ;
568
596
const buffer = host . read ( pkgPath ) ;
569
597
if ( buffer == null ) {
570
598
throw new SchematicsException ( 'Could not read package.json' ) ;
571
599
}
572
- const content = buffer . toString ( ) ;
573
- const pkg = JSON . parse ( content ) ;
600
+ const pkgAst = parseJsonAst ( buffer . toString ( ) , JsonParseMode . Strict ) ;
574
601
575
- if ( pkg === null || typeof pkg !== 'object' || Array . isArray ( pkg ) ) {
602
+ if ( pkgAst . kind != 'object' ) {
576
603
throw new SchematicsException ( 'Error reading package.json' ) ;
577
604
}
578
- if ( ! pkg . devDependencies ) {
579
- pkg . devDependencies = { } ;
605
+
606
+ const devDependenciesNode = findPropertyInAstObject ( pkgAst , 'devDependencies' ) ;
607
+ if ( devDependenciesNode && devDependenciesNode . kind != 'object' ) {
608
+ throw new SchematicsException ( 'Error reading package.json; devDependency is not an object.' ) ;
580
609
}
581
610
582
- pkg . devDependencies [ '@angular-devkit/build-angular' ] = latestVersions . DevkitBuildAngular ;
611
+ const recorder = host . beginUpdate ( pkgPath ) ;
612
+ const depName = '@angular-devkit/build-angular' ;
613
+ if ( ! devDependenciesNode ) {
614
+ // Haven't found the devDependencies key, add it to the root of the package.json.
615
+ appendPropertyInAstObject ( recorder , pkgAst , 'devDependencies' , {
616
+ [ depName ] : latestVersions . DevkitBuildAngular ,
617
+ } ) ;
618
+ } else {
619
+ // Check if there's a build-angular key.
620
+ const buildAngularNode = findPropertyInAstObject ( devDependenciesNode , depName ) ;
621
+
622
+ if ( ! buildAngularNode ) {
623
+ // No build-angular package, add it.
624
+ appendPropertyInAstObject (
625
+ recorder ,
626
+ devDependenciesNode ,
627
+ depName ,
628
+ latestVersions . DevkitBuildAngular ,
629
+ ) ;
630
+ } else {
631
+ const { end, start } = buildAngularNode ;
632
+ recorder . remove ( start . offset , end . offset - start . offset ) ;
633
+ recorder . insertRight ( start . offset , JSON . stringify ( latestVersions . DevkitBuildAngular ) ) ;
634
+ }
635
+ }
583
636
584
- host . overwrite ( pkgPath , JSON . stringify ( pkg , null , 2 ) ) ;
637
+ host . commitUpdate ( recorder ) ;
585
638
586
- if ( packageManager && ! [ 'npm' , 'yarn' , 'cnpm' ] . includes ( packageManager ) ) {
587
- packageManager = undefined ;
588
- }
589
- context . addTask ( new NodePackageInstallTask ( { packageManager } ) ) ;
639
+ context . addTask ( new NodePackageInstallTask ( {
640
+ packageManager : config . packageManager === 'default' ? undefined : config . packageManager ,
641
+ } ) ) ;
590
642
591
643
return host ;
592
644
} ;
@@ -597,19 +649,52 @@ function updateTsLintConfig(): Rule {
597
649
const tsLintPath = '/tslint.json' ;
598
650
const buffer = host . read ( tsLintPath ) ;
599
651
if ( ! buffer ) {
600
- return ;
652
+ return host ;
601
653
}
602
- const tsCfg = JSON . parse ( buffer . toString ( ) ) ;
654
+ const tsCfgAst = parseJsonAst ( buffer . toString ( ) , JsonParseMode . Loose ) ;
603
655
604
- if ( tsCfg . rules && tsCfg . rules [ 'import-blacklist' ] &&
605
- tsCfg . rules [ 'import-blacklist' ] . indexOf ( 'rxjs' ) !== - 1 ) {
656
+ if ( tsCfgAst . kind != 'object' ) {
657
+ return host ;
658
+ }
606
659
607
- tsCfg . rules [ 'import-blacklist' ] = tsCfg . rules [ 'import-blacklist' ]
608
- . filter ( ( rule : string | boolean ) => rule !== 'rxjs' ) ;
660
+ const rulesNode = findPropertyInAstObject ( tsCfgAst , 'rules' ) ;
661
+ if ( ! rulesNode || rulesNode . kind != 'object' ) {
662
+ return host ;
663
+ }
609
664
610
- host . overwrite ( tsLintPath , JSON . stringify ( tsCfg , null , 2 ) ) ;
665
+ const importBlacklistNode = findPropertyInAstObject ( rulesNode , 'import-blacklist' ) ;
666
+ if ( ! importBlacklistNode || importBlacklistNode . kind != 'array' ) {
667
+ return host ;
611
668
}
612
669
670
+ const recorder = host . beginUpdate ( tsLintPath ) ;
671
+ for ( let i = 0 ; i < importBlacklistNode . elements . length ; i ++ ) {
672
+ const element = importBlacklistNode . elements [ i ] ;
673
+ if ( element . kind == 'string' && element . value == 'rxjs' ) {
674
+ const { start, end } = element ;
675
+ // Remove this element.
676
+ if ( i == importBlacklistNode . elements . length - 1 ) {
677
+ // Last element.
678
+ if ( i > 0 ) {
679
+ // Not first, there's a comma to remove before.
680
+ const previous = importBlacklistNode . elements [ i - 1 ] ;
681
+ recorder . remove ( previous . end . offset , end . offset - previous . end . offset ) ;
682
+ } else {
683
+ // Only element, just remove the whole rule.
684
+ const { start, end } = importBlacklistNode ;
685
+ recorder . remove ( start . offset , end . offset - start . offset ) ;
686
+ recorder . insertLeft ( start . offset , '[]' ) ;
687
+ }
688
+ } else {
689
+ // Middle, just remove the whole node (up to next node start).
690
+ const next = importBlacklistNode . elements [ i + 1 ] ;
691
+ recorder . remove ( start . offset , next . start . offset - start . offset ) ;
692
+ }
693
+ }
694
+ }
695
+
696
+ host . commitUpdate ( recorder ) ;
697
+
613
698
return host ;
614
699
} ;
615
700
}
@@ -627,13 +712,17 @@ export default function (): Rule {
627
712
if ( configBuffer == null ) {
628
713
throw new SchematicsException ( `Could not find configuration file (${ configPath } )` ) ;
629
714
}
630
- const config = JSON . parse ( configBuffer . toString ( ) ) ;
715
+ const config = parseJson ( configBuffer . toString ( ) , JsonParseMode . Loose ) ;
716
+
717
+ if ( typeof config != 'object' || Array . isArray ( config ) || config === null ) {
718
+ throw new SchematicsException ( 'Invalid angular-cli.json configuration; expected an object.' ) ;
719
+ }
631
720
632
721
return chain ( [
633
722
migrateKarmaConfiguration ( config ) ,
634
723
migrateConfiguration ( config ) ,
635
724
updateSpecTsConfig ( config ) ,
636
- updatePackageJson ( config . packageManager ) ,
725
+ updatePackageJson ( config ) ,
637
726
updateTsLintConfig ( ) ,
638
727
( host : Tree , context : SchematicContext ) => {
639
728
context . logger . warn ( tags . oneLine `Some configuration options have been changed,
0 commit comments