import { task } from 'gulp' ; import { join , relative } from 'path' ; import { readFileSync } from 'fs' ; import { bold , red , green } from 'chalk' ; import * as ts from 'typescript' ; import * as tsutils from 'tsutils' ; import { buildConfig } from '../../package-tools/build-config' ; const packageVersion = require ( join ( buildConfig . projectDir , 'package.json' ) ) . version . split ( '-' ) [ 0 ] ; const versionRegex = /\d+\.\d+\.\d+/ ; /**
 * Goes through all of the TypeScript files in the project and puts
 * together a summary of all of the pending and expired breaking changes.
 */ task ( 'breaking-changes' , ( ) => { const projectDir = buildConfig . projectDir ; const configFile = ts . readJsonConfigFile ( join ( projectDir , 'tsconfig.json' ) , ts . sys . readFile ) ; const parsedConfig = ts . parseJsonSourceFileConfigFileContent ( configFile , ts . sys , projectDir ) ; const summary = { } ; parsedConfig . fileNames . forEach ( fileName => { const sourceFile = ts . createSourceFile ( fileName , readFileSync ( fileName , 'utf8' ) , configFile . languageVersion ) ; const lineRanges = tsutils . getLineRanges ( sourceFile ) ; tsutils . forEachComment ( sourceFile , ( file , range ) => { const comment = file . substring ( range . pos , range . end ) ; const versionMatch = comment . match ( versionRegex ) ; if ( ! versionMatch || comment . indexOf ( '@breaking-change' ) === - 1 ) { return ; } const pathInProject = relative ( projectDir , sourceFile . fileName ) . replace ( /\\/g , '/' ) ; const [ version ] = versionMatch ; summary [ version ] = summary [ version ] || [ ] ; summary [ version ] . push ( `  ${ pathInProject }: ${ formatMessage ( comment , range , lineRanges ) }` ) ; } ) ; } ) ; Object . keys ( summary ) . forEach ( version => { const isExpired = hasExpired ( packageVersion , version ) ; const status = isExpired ? red ( '(expired)' ) : green ( '(not expired)' ) ; const header = bold ( `Breaking changes for ${ version } ${ status }:` ) ; const messages = summary [ version ] . join ( '\n' ) ; console . log ( isExpired ? red ( header ) : header ) ; console . log ( isExpired ? red ( messages ) : messages , '\n' ) ; } ) ; } ) ; param comment param commentRange param lines function formatMessage ( comment , commentRange , lines ) { const lineNumber = lines . findIndex ( line => line . pos > commentRange . pos ) ; const messageMatch = comment . match ( /@deprecated(.*)|@breaking-change(.*)/ ) ; const message = messageMatch ? messageMatch [ 0 ] : '' ; const cleanMessage = message . replace ( /[\*\/\r\n]|@[\w-]+/g , '' ) . replace ( versionRegex , '' ) . trim ( ) ; return `Line ${ lineNumber }, ${ cleanMessage || 'No message' }` ; } /** Converts a version string into an object. */ function parseVersion ( version ) { const [ major = 0 , minor = 0 , patch = 0 ] = version . split ( '.' ) . map ( segment => parseInt ( segment ) ) ; return { major , minor , patch } ; } param currentVersion param breakingChange function hasExpired ( currentVersion , breakingChange ) { if ( currentVersion === breakingChange ) { return true ; } const current = parseVersion ( currentVersion ) ; const target = parseVersion ( breakingChange ) ; return target . major < current . major || ( target . major === current . major && target . minor < current . minor ) || ( target . major === current . major && target . minor === current . minor && target . patch < current . patch ) ; }