import * as ts from "typescript" ; import { BundleFile } from "../Bundler/BundleResult" ; import { BundleConfig } from "../Bundler/BundleParser" ; import { NodeWalker } from "../Ast/NodeWalker" ; import { Ast } from "../Ast/Ast" ; import { AstTransform } from "../Ast/AstTransform" ; import { StatisticsReporter } from "../Reporting/StatisticsReporter" ; import { Logger } from "../Reporting/Logger" ; import { NameGenerator } from "./NameGenerator" ; import { Container } from "./ContainerContext" ; import { IdentifierInfo } from "./IdentifierInfo" ; import { TsCompilerOptions } from "../Compiler/TsCompilerOptions" ; import { Debug } from "../Utils/Debug" ; import { format } from "../Utils/Formatter" ; import { Utils } from "../Utils/Utilities" ; import { TsCore } from "../Utils/TsCore" ; export class BundleMinifier extends NodeWalker implements AstTransform { private bundleSourceFile : ts . SourceFile ; private program : ts . Program ; private checker : ts . TypeChecker ; private compilerOptions : TsCompilerOptions ; private bundleConfig : BundleConfig ; private containerStack : Container [ ] = [ ] ; private classifiableContainers : ts . MapLike < Container > = { } ; private allIdentifierInfos : ts . MapLike < IdentifierInfo > = { } ; private sourceFileContainer : Container ; private nameGenerator : NameGenerator ; private whiteSpaceBefore : number ; private whiteSpaceAfter : number ; private whiteSpaceTime : number ; private transformTime : number ; private identifierCount = 0 ; private shortenedIdentifierCount = 0 ; constructor ( program , compilerOptions , bundleConfig ) { super ( ) ; this . program = program ; this . checker = program . getTypeChecker ( ) ; this . compilerOptions = compilerOptions ; this . bundleConfig = bundleConfig ; this . containerStack = [ ] ; this . nameGenerator = new NameGenerator ( ) ; } public transform ( bundleSourceFile ) { this . bundleSourceFile = bundleSourceFile ; return this . minify ( bundleSourceFile ) ; } public removeWhitespace ( jsContents ) { this . whiteSpaceTime = new Date ( ) . getTime ( ) ; this . whiteSpaceBefore = jsContents . length ; let output = "" ; let lastNonTriviaToken = ts . SyntaxKind . Unknown ; let isTrivia = false ; let token ; const scanner = ts . createScanner ( ts . ScriptTarget . ES5 , false , ts . LanguageVariant . Standard , jsContents ) ; while ( ( token = scanner . scan ( ) ) !== ts . SyntaxKind . EndOfFileToken ) { isTrivia = false ; if ( Ast . isTrivia ( token ) ) { isTrivia = true ; } if ( ! isTrivia ) { switch ( lastNonTriviaToken ) { case ts . SyntaxKind . FunctionKeyword : if ( token === ts . SyntaxKind . Identifier ) { output += " " ; } break ; case ts . SyntaxKind . BreakKeyword : case ts . SyntaxKind . ContinueKeyword : case ts . SyntaxKind . ReturnKeyword : case ts . SyntaxKind . YieldKeyword : if ( token !== ts . SyntaxKind . SemicolonToken ) { output += " " ; } break ; case ts . SyntaxKind . ElseKeyword : if ( token !== ts . SyntaxKind . OpenBraceToken ) { output += " " ; } break ; } switch ( token ) { case ts . SyntaxKind . CaseKeyword : case ts . SyntaxKind . ClassKeyword : case ts . SyntaxKind . ConstKeyword : case ts . SyntaxKind . DeleteKeyword : case ts . SyntaxKind . DoKeyword : case ts . SyntaxKind . ExportKeyword : case ts . SyntaxKind . GetKeyword : case ts . SyntaxKind . ImportKeyword : case ts . SyntaxKind . LetKeyword : case ts . SyntaxKind . NewKeyword : case ts . SyntaxKind . SetKeyword : case ts . SyntaxKind . StaticKeyword : case ts . SyntaxKind . ThrowKeyword : case ts . SyntaxKind . TypeOfKeyword : case ts . SyntaxKind . VarKeyword : case ts . SyntaxKind . VoidKeyword : output += scanner . getTokenText ( ) + " " ; break ; case ts . SyntaxKind . ExtendsKeyword : case ts . SyntaxKind . InKeyword : case ts . SyntaxKind . InstanceOfKeyword : output += " " + scanner . getTokenText ( ) + " " ; break ; case ts . SyntaxKind . PlusToken : case ts . SyntaxKind . PlusPlusToken : if ( ( lastNonTriviaToken === ts . SyntaxKind . PlusToken ) || ( lastNonTriviaToken === ts . SyntaxKind . PlusPlusToken ) ) { output += " " ; } output += scanner . getTokenText ( ) ; break ; case ts . SyntaxKind . MinusToken : case ts . SyntaxKind . MinusMinusToken : if ( ( lastNonTriviaToken === ts . SyntaxKind . MinusToken ) || ( lastNonTriviaToken === ts . SyntaxKind . MinusMinusToken ) ) { output += " " ; } output += scanner . getTokenText ( ) ; break ; default : output += scanner . getTokenText ( ) ; break ; } } if ( ! isTrivia ) { lastNonTriviaToken = token ; } } this . whiteSpaceAfter = output . length ; this . whiteSpaceTime = new Date ( ) . getTime ( ) - this . whiteSpaceTime ; if ( this . compilerOptions . diagnostics ) this . reportWhitespaceStatistics ( ) ; return output ; } protected visitNode ( node ) { if ( this . isNextContainer ( node ) ) { super . visitNode ( node ) ; this . restoreContainer ( ) ; } else { switch ( node . kind ) { case ts . SyntaxKind . Identifier : Logger . info ( "Identifier node walked in container id: " , this . currentContainer ( ) . getId ( ) ) ; let tmpContainer = this . currentContainer ( ) ; let identifier = < ts . Identifier > node ; let identifierSymbol = this . checker . getSymbolAtLocation ( identifier ) ; if ( ! identifierSymbol ) { identifierSymbol = this . getSymbolFromPrototypeFunction ( identifier ) ; } if ( identifierSymbol ) { let identifierUID = Ast . getIdentifierUID ( identifierSymbol ) ; if ( identifierUID === undefined ) { if ( identifierSymbol . flags & ts . SymbolFlags . Transient ) { Logger . trace ( "Ignoring transient symbol: " , identifierSymbol . name ) ; break ; } else { identifierUID = ( < any > ts ) . getSymbolId ( identifierSymbol ) . toString ( ) ; Logger . trace ( "Generated symbol id for: " , identifierSymbol . name , identifierUID ) ; } } if ( Utils . hasProperty ( this . allIdentifierInfos , identifierUID ) ) { Logger . info ( "Identifier already added: " , identifierSymbol . name , identifierUID ) ; let prevAddedIdentifier = this . allIdentifierInfos [ identifierUID ] ; this . allIdentifierInfos [ identifierUID ] . addRef ( identifier , this . currentContainer ( ) ) ; if ( ! Utils . hasProperty ( this . currentContainer ( ) . localIdentifiers , identifierUID ) ) { this . currentContainer ( ) . excludedIdentifiers [ identifierUID ] = prevAddedIdentifier ; } } else { let identifierInfo = new IdentifierInfo ( identifier , identifierSymbol , this . currentContainer ( ) ) ; Logger . info ( "Adding new identifier: " , identifierInfo . getName ( ) , identifierInfo . getId ( ) ) ; this . currentContainer ( ) . localIdentifiers [ identifierUID ] = identifierInfo ; this . allIdentifierInfos [ identifierUID ] = identifierInfo ; let identifierName = identifierSymbol . getName ( ) ; if ( identifierName . length === 1 ) { identifierInfo . shortenedName = identifierName ; this . currentContainer ( ) . excludedIdentifiers [ identifierUID ] = identifierInfo ; } this . identifierCount ++ ; } } else { Logger . warn ( "Identifier does not have a symbol: " , identifier . text ) ; } break ; } super . visitNode ( node ) ; } } private getSymbolFromPrototypeFunction ( identifier ) { let containerNode = this . currentContainer ( ) . getNode ( ) ; if ( containerNode . kind === ts . SyntaxKind . FunctionExpression ) { if ( Ast . isPrototypeAccessAssignment ( containerNode . parent ) ) { const className = ( ( ( containerNode . parent as ts . BinaryExpression ) . left as ts . PropertyAccessExpression ) . expression as ts . PropertyAccessExpression ) . expression ; const classSymbol = this . checker . getSymbolAtLocation ( className ) ; if ( classSymbol && classSymbol . members ) { if ( classSymbol . members . has ( identifier . escapedText ) ) { Logger . info ( "Symbol obtained from prototype function: " , identifier . text ) ; return classSymbol . members . get ( identifier . escapedText ) ; } } return undefined ; } } return undefined ; } private minify ( sourceFile ) { this . transformTime = new Date ( ) . getTime ( ) ; this . walk ( sourceFile ) ; this . shortenIdentifiers ( ) ; var minifiedSourceFile = ts . createSourceFile ( sourceFile . fileName , sourceFile . text , this . compilerOptions . target ) ; this . transformTime = new Date ( ) . getTime ( ) - this . transformTime ; if ( this . compilerOptions . diagnostics ) this . reportMinifyStatistics ( ) ; return minifiedSourceFile ; } private shortenIdentifiers ( ) { for ( let classContainerKey in this . classifiableContainers ) { let classContainer = this . classifiableContainers [ classContainerKey ] ; let abstractProperties = [ ] ; let heritageProperties = [ ] ; let implementsProperties = [ ] ; let extendsClause = Ast . getExtendsClause ( classContainer . getNode ( ) ) ; if ( extendsClause ) { abstractProperties = Ast . getClassAbstractProperties ( extendsClause , this . checker ) ; } let implementsClause = Ast . getImplementsClause ( classContainer . getNode ( ) ) ; if ( implementsClause ) { implementsProperties = Ast . getImplementsProperties ( implementsClause , this . checker ) ; } heritageProperties = Ast . getClassHeritageProperties ( classContainer . getNode ( ) , this . checker ) ; let excludedProperties = heritageProperties . concat ( abstractProperties , implementsProperties ) ; Logger . trace ( "Class excluded properties for: " , ( < any > classContainer . getNode ( ) ) . name . text , excludedProperties . length , classContainer . getId ( ) ) ; classContainer . excludedProperties = excludedProperties ; } this . shortenContainerIdentifiers ( this . sourceFileContainer ) ; } private shortenContainerIdentifiers ( container ) { let baseClass = container . getBaseClass ( ) ; if ( baseClass ) { let baseClassContainer = this . classifiableContainers [ baseClass . name ] ; if ( baseClassContainer ) { let baseClassMembers = baseClassContainer . getMembers ( ) ; if ( baseClassMembers ) { this . processClassMembers ( baseClassMembers , baseClassContainer ) ; container . excludedProperties = container . excludedProperties . concat ( baseClassContainer . excludedProperties ) ; } } } this . excludeNames ( container ) ; let containerClassMembers = container . getMembers ( ) ; if ( containerClassMembers ) { this . processClassMembers ( containerClassMembers , container ) ; } let containerLocals = container . getLocals ( ) ; if ( containerLocals ) { this . processContainerLocals ( containerLocals , container ) ; } for ( let identifierTableKey in container . localIdentifiers ) { let identifierInfo = container . localIdentifiers [ identifierTableKey ] ; this . processIdentifierInfo ( identifierInfo , container ) ; } for ( let classifiableKey in container . classifiableSymbols ) { let classSymbol = container . classifiableSymbols [ classifiableKey ] ; let classSymbolUId = Ast . getIdentifierUID ( classSymbol ) ; let classIdentifierInfo = this . allIdentifierInfos [ classSymbolUId ] ; this . processIdentifierInfo ( classIdentifierInfo , container ) ; } let containerChildren = container . getChildren ( ) ; for ( let j = 0 ; j < containerChildren . length ; j ++ ) { this . shortenContainerIdentifiers ( containerChildren [ j ] ) ; } } private processIdentifierInfo ( identifierInfo , container ) { if ( this . canShortenIdentifier ( identifierInfo ) ) { let shortenedName = this . getShortenedIdentifierName ( container , identifierInfo ) ; Logger . trace ( "Identifier shortened: " , identifierInfo . getName ( ) , shortenedName ) ; let containerRefs = identifierInfo . getContainers ( ) ; for ( let containerKey in containerRefs ) { let containerRef = containerRefs [ containerKey ] ; containerRef . namesExcluded [ shortenedName ] = true ; } if ( ! identifierInfo . isMinified ) { Utils . forEach ( identifierInfo . getIdentifiers ( ) , identifier => { this . setIdentifierText ( identifier , shortenedName ) ; } ) ; identifierInfo . isMinified = true ; } return ; } } private canShortenIdentifier ( identifierInfo ) { if ( identifierInfo . isBlockScopedVariable ( ) || identifierInfo . isFunctionScopedVariable ( ) || identifierInfo . isInternalClass ( ) || identifierInfo . isInternalInterface ( ) || identifierInfo . isPrivateMethod ( ) || identifierInfo . isPrivateProperty ( ) || identifierInfo . isInternalFunction ( this . bundleConfig . package . getPackageNamespace ( ) ) || identifierInfo . isParameter ( ) || identifierInfo . isNamespaceImportAlias ( ) ) { Logger . trace ( "Identifier CAN be shortened: " , identifierInfo . getName ( ) ) ; return true ; } Logger . trace ( "Identifier CANNOT be shortened: " , identifierInfo . getName ( ) ) ; return false ; } private getShortenedIdentifierName ( container , identifierInfo ) { if ( ! identifierInfo . shortenedName ) { let identifierName = identifierInfo . getName ( ) ; if ( identifierName . length === 1 ) { identifierInfo . shortenedName = identifierName ; } else { while ( true ) { let shortenedName = this . nameGenerator . getName ( container . getNameIndex ( ) ) ; Debug . assert ( shortenedName . length <= identifierName . length ) ; if ( ! Utils . hasProperty ( container . namesExcluded , shortenedName ) ) { identifierInfo . shortenedName = shortenedName ; break ; } else { Logger . trace ( "Generated name was excluded: " , shortenedName , identifierName ) ; } } this . shortenedIdentifierCount ++ ; } } else { Logger . trace ( "Identifier already has shortened name: " , identifierInfo . getName ( ) , identifierInfo . shortenedName ) ; } Logger . info ( "Identifier shortened name: " , identifierInfo . getName ( ) , identifierInfo . shortenedName ) ; return identifierInfo . shortenedName ; } private setIdentifierText ( identifier , text ) { let identifierLength = identifier . text . length ; let bufferLength = ( identifier . end - identifier . pos ) ; var triviaOffset = identifier . getLeadingTriviaWidth ( ) ; for ( var identifierStart = identifier . pos + triviaOffset ; identifierStart < identifier . pos + bufferLength ; identifierStart ++ ) { if ( this . bundleSourceFile . text [ identifierStart ] === identifier . text [ 0 ] ) break ; } ( identifier as ts . TextRange ) . end = identifierStart + text . length ; for ( var i = 0 ; i < identifierLength ; i ++ ) { let replaceChar = " " ; if ( i < text . length ) { replaceChar = text [ i ] ; } this . bundleSourceFile . text = Utils . replaceAt ( this . bundleSourceFile . text , identifierStart + i , replaceChar ) ; } } private processContainerLocals ( locals , container ) { locals . forEach ( local => { let localSymbolUId = Ast . getIdentifierUID ( ( < any > local . declarations [ 0 ] ) . symbol ) ; if ( localSymbolUId ) { let localIdentifierInfo = this . allIdentifierInfos [ localSymbolUId ] ; this . processIdentifierInfo ( localIdentifierInfo , container ) ; } else { Logger . warn ( "Container local does not have a UId" ) ; } } ) ; } private processClassMembers ( members , container ) { for ( let memberKey in members ) { let member = members [ memberKey ] ; let memberSymbol = ( < any > member ) . symbol ; if ( memberSymbol ) { let memberSymbolUId = Ast . getIdentifierUID ( memberSymbol ) ; if ( memberSymbolUId ) { let memberIdentifierInfo = this . allIdentifierInfos [ memberSymbolUId ] ; let isExcludedProperty = false ; for ( const excludedPropertyKey in container . excludedProperties ) { let memberIdentifierSymbol = memberIdentifierInfo . getSymbol ( ) ; let excludedPropertySymbol = container . excludedProperties [ excludedPropertyKey ] ; if ( memberIdentifierSymbol . name === excludedPropertySymbol . name ) { isExcludedProperty = true ; memberIdentifierInfo . shortenedName = memberIdentifierInfo . getName ( ) ; break ; } } if ( ! isExcludedProperty ) { this . processIdentifierInfo ( memberIdentifierInfo , container ) ; } } else { Logger . warn ( "Container member does not have a UId" ) ; } } else { Logger . warn ( "Container member does not have a symbol." ) ; } } } public excludeNames ( container ) { let baseClass = container . getBaseClass ( ) ; if ( baseClass ) { let baseClassContainer = this . classifiableContainers [ baseClass . name ] ; if ( baseClassContainer ) { let baseClassMembers = baseClassContainer . getMembers ( ) ; if ( baseClassMembers ) { for ( let memberKey in baseClassMembers ) { let member = baseClassMembers [ memberKey ] ; let memberSymbol = ( < any > member ) . symbol ; let memberSymbolUId = Ast . getIdentifierUID ( memberSymbol ) ; let excludedIdentifier = this . allIdentifierInfos [ memberSymbolUId ] ; if ( excludedIdentifier && excludedIdentifier . shortenedName ) { container . namesExcluded [ excludedIdentifier . shortenedName ] = true ; } } } } } for ( let identifierInfoKey in container . localIdentifiers ) { let identifierInfo = container . localIdentifiers [ identifierInfoKey ] ; this . excludeNamesForIdentifier ( identifierInfo , container ) ; } for ( let classifiableKey in container . classifiableSymbols ) { let classSymbol = container . classifiableSymbols [ classifiableKey ] ; let classSymbolUId = Ast . getIdentifierUID ( classSymbol ) ; let classIdentifierInfo = this . allIdentifierInfos [ classSymbolUId ] ; Debug . assert ( classIdentifierInfo !== undefined , "Container classifiable identifier symbol not found." ) ; this . excludeNamesForIdentifier ( classIdentifierInfo , container ) ; } } private getContainerExcludedIdentifiers ( container ) { let target = this . compilerOptions . target ; let excludes = { } ; function getContainerExcludes ( container ) { let containerChildren = container . getChildren ( ) ; for ( let i = 0 ; i < containerChildren . length ; i ++ ) { let childContainer = containerChildren [ i ] ; getContainerExcludes ( childContainer ) ; } for ( let excludedIdentifierKey in container . excludedIdentifiers ) { let excludedIdentifier = container . excludedIdentifiers [ excludedIdentifierKey ] ; if ( ( ! excludedIdentifier . isBlockScopedVariable ) || ( target === ts . ScriptTarget . ES5 ) ) { if ( ! Utils . hasProperty ( excludes , excludedIdentifier . getId ( ) ) ) { excludes [ excludedIdentifier . getId ( ) ] = excludedIdentifier ; } } } } getContainerExcludes ( container . getParent ( ) ) ; return excludes ; } private excludeNamesForIdentifier ( identifierInfo , container ) { let identifierContainers = identifierInfo . getContainers ( ) ; for ( let containerKey in identifierContainers ) { let identifierContainer = identifierContainers [ containerKey ] ; let containerExcludes = this . getContainerExcludedIdentifiers ( identifierContainer ) ; for ( let excludedIdentifierKey in containerExcludes ) { let excludedIdentifier = containerExcludes [ excludedIdentifierKey ] ; if ( excludedIdentifier . shortenedName ) { container . namesExcluded [ excludedIdentifier . shortenedName ] = true ; } } } } private currentContainer ( ) { return this . containerStack [ this . containerStack . length - 1 ] ; } private restoreContainer ( ) { return this . containerStack . pop ( ) ; } private isNextContainer ( node ) { let containerFlags = Ast . getContainerFlags ( node ) ; if ( containerFlags & ( Ast . ContainerFlags . IsContainer | Ast . ContainerFlags . IsBlockScopedContainer ) ) { let nextContainer = new Container ( node , containerFlags , this . currentContainer ( ) ) ; let containerSymbol = ( < any > node ) . symbol ; if ( containerSymbol && ( containerSymbol . flags & ts . SymbolFlags . Class ) ) { let containerSymbolUId = Ast . getIdentifierUID ( containerSymbol ) ; if ( ! Utils . hasProperty ( this . currentContainer ( ) . classifiableSymbols , containerSymbolUId ) ) { this . currentContainer ( ) . classifiableSymbols [ containerSymbolUId ] = containerSymbol ; } if ( ! Utils . hasProperty ( this . classifiableContainers , containerSymbol . name ) ) { this . classifiableContainers [ containerSymbol . name ] = nextContainer ; } let extendsClause = Ast . getExtendsClause ( node ) ; if ( extendsClause ) { let baseClassSymbol = this . checker . getSymbolAtLocation ( < ts . Identifier > extendsClause . types [ 0 ] . expression ) ; nextContainer . setBaseClass ( baseClassSymbol ) ; } } let currentContainer = this . currentContainer ( ) ; if ( ! currentContainer ) { this . sourceFileContainer = nextContainer ; } else { currentContainer . addChildContainer ( nextContainer ) ; } this . containerStack . push ( nextContainer ) ; Logger . info ( "Next container id: " , nextContainer . getId ( ) , nextContainer . getParent ( ) . getId ( ) ) ; return true ; } return false ; } private reportWhitespaceStatistics ( ) { let statisticsReporter = new StatisticsReporter ( ) ; statisticsReporter . reportTime ( "Whitespace time" , this . whiteSpaceTime ) ; statisticsReporter . reportPercentage ( "Whitespace reduction" , ( ( this . whiteSpaceBefore - this . whiteSpaceAfter ) / this . whiteSpaceBefore ) * 100.00 ) ; } private reportMinifyStatistics ( ) { let statisticsReporter = new StatisticsReporter ( ) ; statisticsReporter . reportTime ( "Minify time" , this . transformTime ) ; statisticsReporter . reportCount ( "Total identifiers" , this . identifierCount ) ; statisticsReporter . reportCount ( "Identifiers shortened" , this . shortenedIdentifierCount ) ; } private prettify ( input ) { return format ( input ) ; } }