@@ -8,15 +8,23 @@ import { deepEqual } from './diff/util';
8
8
import { IamChanges } from './iam/iam-changes' ;
9
9
import { SecurityGroupChanges } from './network/security-group-changes' ;
10
10
11
+ // tslint:disable-next-line:no-var-requires
12
+ const { structuredPatch } = require ( 'diff' ) ;
13
+
11
14
/**
12
15
* Renders template differences to the process' console.
13
16
*
14
- * @param templateDiff TemplateDiff to be rendered to the console.
17
+ * @param stream The IO stream where to output the rendered diff.
18
+ * @param templateDiff TemplateDiff to be rendered to the console.
15
19
* @param logicalToPathMap A map from logical ID to construct path. Useful in
16
20
* case there is no aws:cdk:path metadata in the template.
21
+ * @param context the number of context lines to use in arbitrary JSON diff (defaults to 3).
17
22
*/
18
- export function formatDifferences ( stream : NodeJS . WriteStream , templateDiff : TemplateDiff , logicalToPathMap : { [ logicalId : string ] : string } = { } ) {
19
- const formatter = new Formatter ( stream , logicalToPathMap , templateDiff ) ;
23
+ export function formatDifferences ( stream : NodeJS . WriteStream ,
24
+ templateDiff : TemplateDiff ,
25
+ logicalToPathMap : { [ logicalId : string ] : string } = { } ,
26
+ context : number = 3 ) {
27
+ const formatter = new Formatter ( stream , logicalToPathMap , templateDiff , context ) ;
20
28
21
29
if ( templateDiff . awsTemplateFormatVersion || templateDiff . transform || templateDiff . description ) {
22
30
formatter . printSectionHeader ( 'Template' ) ;
@@ -40,8 +48,11 @@ export function formatDifferences(stream: NodeJS.WriteStream, templateDiff: Temp
40
48
/**
41
49
* Renders a diff of security changes to the given stream
42
50
*/
43
- export function formatSecurityChanges ( stream : NodeJS . WriteStream , templateDiff : TemplateDiff , logicalToPathMap : { [ logicalId : string ] : string } = { } ) {
44
- const formatter = new Formatter ( stream , logicalToPathMap , templateDiff ) ;
51
+ export function formatSecurityChanges ( stream : NodeJS . WriteStream ,
52
+ templateDiff : TemplateDiff ,
53
+ logicalToPathMap : { [ logicalId : string ] : string } = { } ,
54
+ context ?: number ) {
55
+ const formatter = new Formatter ( stream , logicalToPathMap , templateDiff , context ) ;
45
56
46
57
formatSecurityChangesWithBanner ( formatter , templateDiff ) ;
47
58
}
@@ -56,11 +67,15 @@ function formatSecurityChangesWithBanner(formatter: Formatter, templateDiff: Tem
56
67
}
57
68
58
69
const ADDITION = colors . green ( '[+]' ) ;
70
+ const CONTEXT = colors . grey ( '[ ]' ) ;
59
71
const UPDATE = colors . yellow ( '[~]' ) ;
60
72
const REMOVAL = colors . red ( '[-]' ) ;
61
73
62
74
class Formatter {
63
- constructor ( private readonly stream : NodeJS . WriteStream , private readonly logicalToPathMap : { [ logicalId : string ] : string } , diff ?: TemplateDiff ) {
75
+ constructor ( private readonly stream : NodeJS . WriteStream ,
76
+ private readonly logicalToPathMap : { [ logicalId : string ] : string } ,
77
+ diff ?: TemplateDiff ,
78
+ private readonly context : number = 3 ) {
64
79
// Read additional construct paths from the diff if it is supplied
65
80
if ( diff ) {
66
81
this . readConstructPathsFrom ( diff ) ;
@@ -126,7 +141,7 @@ class Formatter {
126
141
* Print a resource difference for a given logical ID.
127
142
*
128
143
* @param logicalId the logical ID of the resource that changed.
129
- * @param diff the change to be rendered.
144
+ * @param diff the change to be rendered.
130
145
*/
131
146
public formatResourceDifference ( _type : string , logicalId : string , diff : ResourceDifference ) {
132
147
const resourceType = diff . isRemoval ? diff . oldResourceType : diff . newResourceType ;
@@ -184,9 +199,9 @@ class Formatter {
184
199
185
200
/**
186
201
* Renders a tree of differences under a particular name.
187
- * @param name the name of the root of the tree.
188
- * @param diff the difference on the tree.
189
- * @param last whether this is the last node of a parent tree.
202
+ * @param name the name of the root of the tree.
203
+ * @param diff the difference on the tree.
204
+ * @param last whether this is the last node of a parent tree.
190
205
*/
191
206
public formatTreeDiff ( name : string , diff : Difference < any > , last : boolean ) {
192
207
let additionalInfo = '' ;
@@ -210,10 +225,19 @@ class Formatter {
210
225
* @param linePrefix a prefix (indent-like) to be used on every line.
211
226
*/
212
227
public formatObjectDiff ( oldObject : any , newObject : any , linePrefix : string ) {
213
- if ( ( typeof oldObject !== typeof newObject ) || Array . isArray ( oldObject ) || typeof oldObject === 'string' || typeof oldObject === 'number' ) {
228
+ if ( ( typeof oldObject !== typeof newObject ) || Array . isArray ( oldObject ) || typeof oldObject === 'string' || typeof oldObject === 'number' ) {
214
229
if ( oldObject !== undefined && newObject !== undefined ) {
215
- this . print ( '%s ├─ %s %s' , linePrefix , REMOVAL , this . formatValue ( oldObject , colors . red ) ) ;
216
- this . print ( '%s └─ %s %s' , linePrefix , ADDITION , this . formatValue ( newObject , colors . green ) ) ;
230
+ if ( typeof oldObject === 'object' || typeof newObject === 'object' ) {
231
+ const oldStr = JSON . stringify ( oldObject , null , 2 ) ;
232
+ const newStr = JSON . stringify ( newObject , null , 2 ) ;
233
+ const diff = _diffStrings ( oldStr , newStr , this . context ) ;
234
+ for ( let i = 0 ; i < diff . length ; i ++ ) {
235
+ this . print ( '%s %s %s' , linePrefix , i === 0 ? '└─' : ' ' , diff [ i ] ) ;
236
+ }
237
+ } else {
238
+ this . print ( '%s ├─ %s %s' , linePrefix , REMOVAL , this . formatValue ( oldObject , colors . red ) ) ;
239
+ this . print ( '%s └─ %s %s' , linePrefix , ADDITION , this . formatValue ( newObject , colors . green ) ) ;
240
+ }
217
241
} else if ( oldObject !== undefined /* && newObject === undefined */ ) {
218
242
this . print ( '%s └─ %s' , linePrefix , this . formatValue ( oldObject , colors . red ) ) ;
219
243
} else /* if (oldObject === undefined && newObject !== undefined) */ {
@@ -398,3 +422,75 @@ function stripHorizontalLines(tableRendering: string) {
398
422
return cols [ 1 ] ;
399
423
}
400
424
}
425
+
426
+ /**
427
+ * A patch as returned by ``diff.structuredPatch``.
428
+ */
429
+ interface Patch {
430
+ /**
431
+ * Hunks in the patch.
432
+ */
433
+ hunks : ReadonlyArray < PatchHunk > ;
434
+ }
435
+
436
+ /**
437
+ * A hunk in a patch produced by ``diff.structuredPatch``.
438
+ */
439
+ interface PatchHunk {
440
+ oldStart : number ;
441
+ oldLines : number ;
442
+ newStart : number ;
443
+ newLines : number ;
444
+ lines : string [ ] ;
445
+ }
446
+
447
+ /**
448
+ * Creates a unified diff of two strings.
449
+ *
450
+ * @param oldStr the "old" version of the string.
451
+ * @param newStr the "new" version of the string.
452
+ * @param context the number of context lines to use in arbitrary JSON diff.
453
+ *
454
+ * @returns an array of diff lines.
455
+ */
456
+ function _diffStrings ( oldStr : string , newStr : string , context : number ) : string [ ] {
457
+ const patch : Patch = structuredPatch ( null , null , oldStr , newStr , null , null , { context } ) ;
458
+ const result = new Array < string > ( ) ;
459
+ for ( const hunk of patch . hunks ) {
460
+ result . push ( colors . magenta ( `@@ -${ hunk . oldStart } ,${ hunk . oldLines } +${ hunk . newStart } ,${ hunk . newLines } @@` ) ) ;
461
+ const baseIndent = _findIndent ( hunk . lines ) ;
462
+ for ( const line of hunk . lines ) {
463
+ // Don't care about termination newline.
464
+ if ( line === '\\ No newline at end of file' ) { continue ; }
465
+ const marker = line . charAt ( 0 ) ;
466
+ const text = line . slice ( 1 + baseIndent ) ;
467
+ switch ( marker ) {
468
+ case ' ' :
469
+ result . push ( `${ CONTEXT } ${ text } ` ) ;
470
+ break ;
471
+ case '+' :
472
+ result . push ( colors . bold ( `${ ADDITION } ${ colors . green ( text ) } ` ) ) ;
473
+ break ;
474
+ case '-' :
475
+ result . push ( colors . bold ( `${ REMOVAL } ${ colors . red ( text ) } ` ) ) ;
476
+ break ;
477
+ default :
478
+ throw new Error ( `Unexpected diff marker: ${ marker } (full line: ${ line } )` ) ;
479
+ }
480
+ }
481
+ }
482
+ return result ;
483
+
484
+ function _findIndent ( lines : string [ ] ) : number {
485
+ let indent = Number . MAX_SAFE_INTEGER ;
486
+ for ( const line of lines ) {
487
+ for ( let i = 1 ; i < line . length ; i ++ ) {
488
+ if ( line . charAt ( i ) !== ' ' ) {
489
+ indent = indent > i - 1 ? i - 1 : indent ;
490
+ break ;
491
+ }
492
+ }
493
+ }
494
+ return indent ;
495
+ }
496
+ }
0 commit comments