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,90 @@ 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
+ appendPropertyInAstObject ( recorder , tsCfgAst , 'files' , [ polyfills ] ) ;
581
+ } else {
582
+ if ( filesAstNode . value . indexOf ( polyfills ) == - 1 ) {
583
+ appendValueInAstArray ( recorder , filesAstNode , polyfills ) ;
584
+ }
585
+ }
586
+
587
+ host . commitUpdate ( recorder ) ;
561
588
} ) ;
562
589
} ;
563
590
}
564
591
565
- function updatePackageJson ( packageManager ?: string ) {
592
+ function updatePackageJson ( config : CliConfig ) {
566
593
return ( host : Tree , context : SchematicContext ) => {
567
594
const pkgPath = '/package.json' ;
568
595
const buffer = host . read ( pkgPath ) ;
569
596
if ( buffer == null ) {
570
597
throw new SchematicsException ( 'Could not read package.json' ) ;
571
598
}
572
- const content = buffer . toString ( ) ;
573
- const pkg = JSON . parse ( content ) ;
599
+ const pkgAst = parseJsonAst ( buffer . toString ( ) , JsonParseMode . Strict ) ;
574
600
575
- if ( pkg === null || typeof pkg !== 'object' || Array . isArray ( pkg ) ) {
601
+ if ( pkgAst . kind != 'object' ) {
576
602
throw new SchematicsException ( 'Error reading package.json' ) ;
577
603
}
578
- if ( ! pkg . devDependencies ) {
579
- pkg . devDependencies = { } ;
604
+
605
+ const devDependenciesNode = findPropertyInAstObject ( pkgAst , 'devDependencies' ) ;
606
+ if ( devDependenciesNode && devDependenciesNode . kind != 'object' ) {
607
+ throw new SchematicsException ( 'Error reading package.json; devDependency is not an object.' ) ;
580
608
}
581
609
582
- pkg . devDependencies [ '@angular-devkit/build-angular' ] = latestVersions . DevkitBuildAngular ;
610
+ const recorder = host . beginUpdate ( pkgPath ) ;
611
+ const depName = '@angular-devkit/build-angular' ;
612
+ if ( ! devDependenciesNode ) {
613
+ // Haven't found the devDependencies key, add it to the root of the package.json.
614
+ appendPropertyInAstObject ( recorder , pkgAst , 'devDependencies' , {
615
+ [ depName ] : latestVersions . DevkitBuildAngular ,
616
+ } ) ;
617
+ } else {
618
+ // Check if there's a build-angular key.
619
+ const buildAngularNode = findPropertyInAstObject ( devDependenciesNode , depName ) ;
620
+
621
+ if ( ! buildAngularNode ) {
622
+ // No build-angular package, add it.
623
+ appendPropertyInAstObject (
624
+ recorder ,
625
+ devDependenciesNode ,
626
+ depName ,
627
+ latestVersions . DevkitBuildAngular ,
628
+ ) ;
629
+ } else {
630
+ const { end, start } = buildAngularNode ;
631
+ recorder . remove ( start . offset , end . offset - start . offset ) ;
632
+ recorder . insertRight ( start . offset , JSON . stringify ( latestVersions . DevkitBuildAngular ) ) ;
633
+ }
634
+ }
583
635
584
- host . overwrite ( pkgPath , JSON . stringify ( pkg , null , 2 ) ) ;
636
+ host . commitUpdate ( recorder ) ;
585
637
586
- if ( packageManager && ! [ 'npm' , 'yarn' , 'cnpm' ] . includes ( packageManager ) ) {
587
- packageManager = undefined ;
588
- }
589
- context . addTask ( new NodePackageInstallTask ( { packageManager } ) ) ;
638
+ context . addTask ( new NodePackageInstallTask ( {
639
+ packageManager : config . packageManager === 'default' ? undefined : config . packageManager ,
640
+ } ) ) ;
590
641
591
642
return host ;
592
643
} ;
@@ -597,19 +648,52 @@ function updateTsLintConfig(): Rule {
597
648
const tsLintPath = '/tslint.json' ;
598
649
const buffer = host . read ( tsLintPath ) ;
599
650
if ( ! buffer ) {
600
- return ;
651
+ return host ;
601
652
}
602
- const tsCfg = JSON . parse ( buffer . toString ( ) ) ;
653
+ const tsCfgAst = parseJsonAst ( buffer . toString ( ) , JsonParseMode . Loose ) ;
603
654
604
- if ( tsCfg . rules && tsCfg . rules [ 'import-blacklist' ] &&
605
- tsCfg . rules [ 'import-blacklist' ] . indexOf ( 'rxjs' ) !== - 1 ) {
655
+ if ( tsCfgAst . kind != 'object' ) {
656
+ return host ;
657
+ }
606
658
607
- tsCfg . rules [ 'import-blacklist' ] = tsCfg . rules [ 'import-blacklist' ]
608
- . filter ( ( rule : string | boolean ) => rule !== 'rxjs' ) ;
659
+ const rulesNode = findPropertyInAstObject ( tsCfgAst , 'rules' ) ;
660
+ if ( ! rulesNode || rulesNode . kind != 'object' ) {
661
+ return host ;
662
+ }
609
663
610
- host . overwrite ( tsLintPath , JSON . stringify ( tsCfg , null , 2 ) ) ;
664
+ const importBlacklistNode = findPropertyInAstObject ( rulesNode , 'import-blacklist' ) ;
665
+ if ( ! importBlacklistNode || importBlacklistNode . kind != 'array' ) {
666
+ return host ;
611
667
}
612
668
669
+ const recorder = host . beginUpdate ( tsLintPath ) ;
670
+ for ( let i = 0 ; i < importBlacklistNode . elements . length ; i ++ ) {
671
+ const element = importBlacklistNode . elements [ i ] ;
672
+ if ( element . kind == 'string' && element . value == 'rxjs' ) {
673
+ const { start, end } = element ;
674
+ // Remove this element.
675
+ if ( i == importBlacklistNode . elements . length - 1 ) {
676
+ // Last element.
677
+ if ( i > 0 ) {
678
+ // Not first, there's a comma to remove before.
679
+ const previous = importBlacklistNode . elements [ i - 1 ] ;
680
+ recorder . remove ( previous . end . offset , end . offset - previous . end . offset ) ;
681
+ } else {
682
+ // Only element, just remove the whole rule.
683
+ const { start, end } = importBlacklistNode ;
684
+ recorder . remove ( start . offset , end . offset - start . offset ) ;
685
+ recorder . insertLeft ( start . offset , '[]' ) ;
686
+ }
687
+ } else {
688
+ // Middle, just remove the whole node (up to next node start).
689
+ const next = importBlacklistNode . elements [ i + 1 ] ;
690
+ recorder . remove ( start . offset , next . start . offset - start . offset ) ;
691
+ }
692
+ }
693
+ }
694
+
695
+ host . commitUpdate ( recorder ) ;
696
+
613
697
return host ;
614
698
} ;
615
699
}
@@ -627,13 +711,17 @@ export default function (): Rule {
627
711
if ( configBuffer == null ) {
628
712
throw new SchematicsException ( `Could not find configuration file (${ configPath } )` ) ;
629
713
}
630
- const config = JSON . parse ( configBuffer . toString ( ) ) ;
714
+ const config = parseJson ( configBuffer . toString ( ) , JsonParseMode . Loose ) ;
715
+
716
+ if ( typeof config != 'object' || Array . isArray ( config ) || config === null ) {
717
+ throw new SchematicsException ( 'Invalid angular-cli.json configuration; expected an object.' ) ;
718
+ }
631
719
632
720
return chain ( [
633
721
migrateKarmaConfiguration ( config ) ,
634
722
migrateConfiguration ( config ) ,
635
723
updateSpecTsConfig ( config ) ,
636
- updatePackageJson ( config . packageManager ) ,
724
+ updatePackageJson ( config ) ,
637
725
updateTsLintConfig ( ) ,
638
726
( host : Tree , context : SchematicContext ) => {
639
727
context . logger . warn ( tags . oneLine `Some configuration options have been changed,
0 commit comments