import * as path from "path" ; import * as readline from "readline" ; import * as ts from "typescript" ; import * as protobuf from "google-protobuf" ; import * as minimist from "minimist" ; import * as worker from "../lib/worker_protocol_pb" ; interface Settings extends minimist . ParsedArgs { max_compiles : number ; max_idle_seconds : number ; persistent_worker ? : boolean ; debug ? : boolean ; } let settings = { max_compiles : 10 , max_idle_seconds : 5 , _ : [ ] } ; let parsedCommandLine ; const languageServiceHost = { getCompilationSettings : ( ) : ts . CompilerOptions => parsedCommandLine . options , getNewLine : ( ) => ts . sys . newLine , getScriptFileNames : ( ) : string [ ] => parsedCommandLine . fileNames , getScriptVersion : ( fileName ) : string => { return ts . sys . getFileSize ( fileName ) + ":" + ts . sys . getModifiedTime ( fileName ) . getTime ( ) ; } , getScriptSnapshot : ( fileName ) : ts . IScriptSnapshot | undefined => { if ( ! ts . sys . fileExists ( fileName ) ) { return undefined ; } let text = ts . sys . readFile ( fileName ) ; return { getText : ( start , end ) => { if ( start === 0 && end === text . length ) { return text ; } else { return text . slice ( start , end ) ; } } , getLength : ( ) => text . length , getChangeRange : ( oldSnapshot ) : ts . TextChangeRange | undefined => { const oldText = oldSnapshot . getText ( 0 , oldSnapshot . getLength ( ) ) ; let firstDiff = 0 ; while ( firstDiff < oldText . length && firstDiff < text . length && text [ firstDiff ] === oldText [ firstDiff ] ) { firstDiff ++ ; } let oldIndex = oldText . length ; let newIndex = text . length ; while ( oldIndex > firstDiff && newIndex > firstDiff && oldText [ oldIndex - 1 ] === text [ newIndex - 1 ] ) { oldIndex -- ; newIndex -- ; } return { span : { start : firstDiff , length : oldIndex - firstDiff } , newLength : newIndex - firstDiff } ; } , dispose : ( ) => { text = "" ; } } ; } , getCurrentDirectory : ts . sys . getCurrentDirectory , getDefaultLibFileName : ts . getDefaultLibFilePath } ; const formatDiagnosticsHost = { getCurrentDirectory : ts . sys . getCurrentDirectory , getCanonicalFileName : ( fileName ) => fileName , getNewLine : ( ) => ts . sys . newLine } ; class LanguageServiceProvider { private _languageService : ts . LanguageService ; private _count = 0 ; private _idleTimer : NodeJS . Timer ; acquire ( ) { this . _clearIdleTimeout ( ) ; if ( ! this . _languageService ) { this . _languageService = ts . createLanguageService ( languageServiceHost ) ; this . _count = 0 ; } ++ this . _count ; return this . _languageService ; } release ( ) { if ( this . _languageService ) { if ( this . _count >= settings . max_compiles ) { this . _freeMemory ( ) ; } else { this . _setIdleTimeout ( ) ; } } } private _setIdleTimeout ( ) { if ( settings . max_idle_seconds ) { this . _idleTimer = setTimeout ( this . _onIdle , settings . max_idle_seconds * 1000 ) ; } } private _clearIdleTimeout ( ) { if ( this . _idleTimer ) { clearTimeout ( this . _idleTimer ) ; this . _idleTimer = null ; } } private _onIdle = ( ) => { this . _idleTimer = null ; this . _freeMemory ( ) ; } ; private _freeMemory ( ) { this . _languageService = null ; if ( global . gc ) { global . gc ( ) ; } } } const languageServiceProvider = new LanguageServiceProvider ( ) ; function fileNotFoundDiagnostic ( filename ) { return { file : undefined , start : undefined , length : undefined , messageText : `File '${ filename }' not found.` , category : ts . DiagnosticCategory . Error , code : 6053 } ; } function ensureRootFilesExist ( filenames ) { return filenames . filter ( filename => ! ts . sys . fileExists ( filename ) ) . map ( filename => fileNotFoundDiagnostic ( filename ) ) ; } function parseCommandLine ( args ) { let pcl = ts . parseCommandLine ( args ) ; const configFileOrDirectory = ( args . length > 0 ) ? pcl . options . project : "." ; if ( configFileOrDirectory ) { if ( pcl . fileNames . length > 0 ) { pcl . errors . push ( { file : undefined , start : undefined , length : undefined , messageText : "Option 'project' cannot be mixed with source files on a command line." , category : ts . DiagnosticCategory . Error , code : 5042 } ) ; return pcl ; } const configFileName = ts . sys . directoryExists ( configFileOrDirectory ) ? path . join ( configFileOrDirectory , "tsconfig.json" ) : configFileOrDirectory ; if ( ! ts . sys . fileExists ( configFileName ) ) { pcl . errors . push ( fileNotFoundDiagnostic ( configFileName ) ) ; return pcl ; } const parseConfigFileHost = { useCaseSensitiveFileNames : ts . sys . useCaseSensitiveFileNames , readDirectory : ts . sys . readDirectory , fileExists : ts . sys . fileExists , readFile : ts . sys . readFile , getCurrentDirectory : ts . sys . getCurrentDirectory , onUnRecoverableConfigFileDiagnostic : diagnostic => { pcl . errors . push ( diagnostic ) ; } } ; pcl = ts . getParsedCommandLineOfConfigFile ( configFileName , pcl . options , parseConfigFileHost ) ; } return pcl ; } function compile ( args ) { let exitCode = ts . ExitStatus . DiagnosticsPresent_OutputsSkipped ; let output = "" ; try { let emitSkipped = true ; const diagnostics = [ ] ; parsedCommandLine = parseCommandLine ( args ) ; if ( parsedCommandLine . errors . length > 0 ) { diagnostics . push ( ... parsedCommandLine . errors ) ; } else if ( parsedCommandLine . options . version ) { output += `Version ${ ts . version }\n` ; } else { diagnostics . push ( ... ensureRootFilesExist ( parsedCommandLine . fileNames ) ) ; if ( diagnostics . length === 0 ) { let languageService ; try { languageService = languageServiceProvider . acquire ( ) ; const program = languageService . getProgram ( ) ; diagnostics . push ( ... ts . getPreEmitDiagnostics ( program ) ) ; if ( diagnostics . length === 0 ) { const compilerHost = ts . createCompilerHost ( parsedCommandLine . options ) ; const emitOutput = program . emit ( undefined , compilerHost . writeFile ) ; diagnostics . push ( ... emitOutput . diagnostics ) ; emitSkipped = emitOutput . emitSkipped ; } } finally { languageServiceProvider . release ( ) ; } } } output += ts . formatDiagnostics ( diagnostics , formatDiagnosticsHost ) ; if ( emitSkipped ) { exitCode = ts . ExitStatus . DiagnosticsPresent_OutputsSkipped ; } else if ( diagnostics . length > 0 ) { exitCode = ts . ExitStatus . DiagnosticsPresent_OutputsGenerated ; } else { exitCode = ts . ExitStatus . Success ; } } catch ( e ) { exitCode = ts . ExitStatus . DiagnosticsPresent_OutputsSkipped ; output = "" + e . stack ; } return { exitCode , output } ; } function readUnsignedVarint32 ( a ) { let b ; let result = 0 ; let offset = 0 ; for ( let i = 0 ; i < 5 ; i ++ ) { b = a [ offset ++ ] ; result |= ( b & 0x7F ) << ( 7 * i ) ; if ( ! ( b & 0x80 ) ) { break ; } } return { value : result , length : offset } ; } function persistentWorker ( exit ) { let data = null ; let dataOffset = 0 ; process . stdin . on ( "data" , ( chunk ) => { if ( data === null ) { const varint = readUnsignedVarint32 ( chunk ) ; const messageSize = varint . value ; if ( messageSize <= chunk . length - varint . length ) { data = chunk . slice ( varint . length , varint . length + varint . value ) ; dataOffset = data . length ; } else { data = Buffer . allocUnsafe ( messageSize ) ; dataOffset = chunk . copy ( data , 0 , varint . length ) ; } } else { dataOffset += chunk . copy ( data , dataOffset ) ; } if ( dataOffset < data . length ) { return ; } const reader = new protobuf . BinaryReader ( new Uint8Array ( data ) ) ; const workRequest = new worker . WorkRequest ( ) ; worker . WorkRequest . deserializeBinaryFromReader ( workRequest , reader ) ; data = null ; dataOffset = 0 ; const args = workRequest . getArgumentsList ( ) ; const { exitCode , output } = compile ( args ) ; const workResponse = new worker . WorkResponse ( ) ; workResponse . setExitCode ( exitCode ) ; workResponse . setOutput ( output ) ; const workResponseBytes = workResponse . serializeBinary ( ) ; const writer = new protobuf . BinaryWriter ( ) ; const encoder_ = ( writer as any ) . encoder_ ; encoder_ . writeUnsignedVarint32 ( workResponseBytes . length ) ; const lengthArray = encoder_ . end ( ) ; const buffer = new Buffer ( lengthArray . length + workResponseBytes . length ) ; buffer . set ( lengthArray ) ; buffer . set ( workResponseBytes , lengthArray . length ) ; process . stdout . write ( new Buffer ( buffer ) ) ; } ) ; process . stdin . on ( "end" , ( ) => { exit ( 0 ) ; } ) ; } function main ( args , exit ) { settings = { ... settings , ... minimist < Settings > ( args ) } ; if ( settings [ "persistent_worker" ] ) { persistentWorker ( exit ) ; } else if ( settings [ "debug" ] ) { const rl = readline . createInterface ( { input : process . stdin , output : process . stdout } ) ; rl . prompt ( ) ; rl . on ( "line" , ( input ) => { const startTime = process . hrtime ( ) ; const cmdArgs = input . split ( " " ) ; const { exitCode , output } = compile ( cmdArgs ) ; const elapsedTime = process . hrtime ( startTime ) ; const elapsedMillis = Math . floor ( ( elapsedTime [ 0 ] * 1e9 + elapsedTime [ 1 ] ) / 1e6 ) ; let message = `Compilation took ${ elapsedMillis }ms. Exit code: ${ exitCode }.` ; if ( output ) { message += ` Compiler output:\n${ output }` ; } else { message += "\n" ; } process . stdout . write ( message ) ; rl . prompt ( ) ; } ) ; rl . on ( "close" , ( ) => { exit ( 0 ) ; } ) ; } else { const { exitCode , output } = compile ( args ) ; if ( output ) { process . stdout . write ( output ) ; } exit ( exitCode ) ; } } main ( ts . sys . args , ts . sys . exit ) ;