import { Program , Diagnostic , SourceFile , CompilerOptions , DiagnosticCategory , EmitResult , convertCompilerOptionsFromJson , createProgram , createCompilerHost , getPreEmitDiagnostics , flattenDiagnosticMessageText , sys } from "typescript" ; import * as fs from "fs-extra" ; const args = parseArgs ( process . argv ) ; const sbtTypescriptOpts = args . options ; const logger = new Logger ( sbtTypescriptOpts . logLevel ) ; const sourceMappings = new SourceMappings ( args . sourceFileMappings ) ; logger . debug ( "starting compilation of " , sourceMappings . mappings . map ( ( sm ) => sm . relativePath ) ) ; logger . debug ( "from " , sbtTypescriptOpts . assetsDirs ) ; logger . debug ( "to " , args . target ) ; logger . debug ( "args " + JSON . stringify ( args , null , 2 ) ) ; const compileResult = compile ( sourceMappings , sbtTypescriptOpts , args . target ) ; compileDone ( compileResult ) ; function compile ( sourceMaps , sbtOptions , target ) { const problems = [ ] ; let results = [ ] ; const { options : compilerOptions , errors } = toCompilerOptions ( sbtOptions ) ; if ( errors . length > 0 ) { problems . push ( ... toProblems ( errors , sbtOptions . tsCodesToIgnore ) ) ; } else { compilerOptions . outDir = target ; let nodeModulesPaths = [ ] ; if ( sbtOptions . resolveFromNodeModulesDir ) { nodeModulesPaths = nodeModulesPaths . concat ( sbtOptions . nodeModulesDirs . map ( p => p + "/*" ) ) ; nodeModulesPaths = nodeModulesPaths . concat ( sbtOptions . nodeModulesDirs . map ( p => p + "/@types/*" ) ) ; compilerOptions . typeRoots = sbtOptions . nodeModulesDirs . map ( p => p + "/@types" ) ; } const assetPaths = sbtOptions . assetsDirs . map ( p => p + "/*" ) ; compilerOptions . baseUrl = "." ; compilerOptions . paths = { "*" : [ "*" ] . concat ( nodeModulesPaths ) } ; logger . debug ( "using tsc options " , compilerOptions ) ; const compilerHost = createCompilerHost ( compilerOptions ) ; let filesToCompile = sourceMaps . asAbsolutePaths ( ) ; if ( sbtOptions . extraFiles ) filesToCompile = filesToCompile . concat ( sbtOptions . extraFiles ) ; logger . debug ( "files to compile " , filesToCompile ) ; const program = createProgram ( filesToCompile , compilerOptions , compilerHost ) ; logger . debug ( "created program" ) ; problems . push ( ... findPreemitProblems ( program , sbtOptions . tsCodesToIgnore ) ) ; const emitOutput = program . emit ( ) ; const moveTestPromise = sbtOptions . assetsDirs . length === 2 ? moveEmittedTestAssets ( sbtOptions ) : Promise . resolve ( { } ) ; moveTestPromise . then ( ( value ) => { if ( sbtOptions . assertCompilation ) { logAndAssertEmitted ( results , emitOutput ) ; } } , ( e ) => { } ) ; problems . push ( ... toProblems ( emitOutput . diagnostics , sbtOptions . tsCodesToIgnore ) ) ; if ( logger . isDebug ) { const declarationFiles = program . getSourceFiles ( ) . filter ( isDeclarationFile ) ; logger . debug ( "referring to " + declarationFiles . length + " declaration files and " + ( program . getSourceFiles ( ) . length - declarationFiles . length ) + " code files." ) ; } if ( ! emitOutput . emitSkipped ) { results = flatten ( program . getSourceFiles ( ) . filter ( isCodeFile ) . map ( toCompilationResult ( sourceMaps , compilerOptions ) ) ) ; } else { results = [ ] ; } } const output = < CompilationResult > { results : results , problems : problems } ; return output ; function logAndAssertEmitted ( declaredResults , emitOutput ) { const ffw = flatFilesWritten ( declaredResults ) ; const emitted = emitOutput . emitSkipped ? [ ] : emitOutput . emittedFiles ; logger . debug ( "files written" , ffw ) ; logger . debug ( "files emitted" , emitted ) ; const emittedButNotDeclared = minus ( emitted , ffw ) ; const declaredButNotEmitted = minus ( ffw , emitted ) ; notExistingFiles ( ffw ) . then ( nef => { if ( nef . length > 0 ) { logger . error ( `files declared that have not been generated ${ nef }` ) ; } else { logger . debug ( `all declared files exist` ) ; } } ) . catch ( err => logger . error ( "unexpected error" , err ) ) ; if ( emittedButNotDeclared . length > 0 || declaredButNotEmitted . length > 0 ) { const errorMessage = `
emitted and declared files are not equal
emitted but not declared ${ emittedButNotDeclared }
declared but not emitted ${ declaredButNotEmitted }
` ; if ( ! emitOutput . emitSkipped ) logger . error ( errorMessage ) ; } return ; function minus ( arr1 , arr2 ) { const r = [ ] ; for ( let s of arr1 ) { if ( arr2 . indexOf ( s ) == - 1 ) { r . push ( s ) ; } } return r ; } } function moveEmittedTestAssets ( sbtOpts ) { const common = commonPath ( sbtOpts . assetsDirs [ 0 ] , sbtOpts . assetsDirs [ 1 ] ) ; const relPathAssets = sbtOpts . assetsDirs [ 0 ] . substring ( common . length ) ; const relPathTestAssets = sbtOpts . assetsDirs [ 1 ] . substring ( common . length ) ; const sourcePath = path . join ( target , relPathTestAssets ) ; const moveMsg = `${ sourcePath } to ${ target }` ; return Promise . all ( [ remove ( path . join ( target , relPathAssets ) ) , move ( sourcePath , target ) ] ) ; } function remove ( dir ) { return new Promise ( ( resolve , reject ) => { fs . remove ( dir , ( e ) => { if ( e ) { reject ( e ) ; } else { logger . debug ( "removed" , dir ) ; resolve ( { } ) ; } } ) ; } ) ; } function move ( sourcePath , target ) { return new Promise ( ( resolve , reject ) => { fs . copy ( sourcePath , target , ( e ) => { if ( e ) { reject ( e ) ; } else { fs . remove ( sourcePath , ( e ) => { if ( e ) { reject ( e ) ; } else { logger . debug ( `moved contents of ${ sourcePath } to ${ target }` ) ; resolve ( { } ) ; } } ) ; } } ) ; } ) ; } function notExistingFiles ( filesDeclared ) { return Promise . all ( filesDeclared . map ( exists ) ) . then ( ( e ) => { const r = e . filter ( a => { const [ s , exist ] = a ; return ! exist ; } ) . map ( a => { const [ s , b ] = a ; return s ; } ) ; return r ; } ) ; function exists ( file ) { return new Promise < [ string , boolean ] > ( ( resolve , reject ) => { fs . access ( file , ( errAccess ) => { if ( errAccess ) { resolve ( [ file , false ] ) ; } else { fs . stat ( file , ( err , stats ) => { if ( err ) { reject ( err ) ; } else { resolve ( [ file , stats . isFile ( ) ] ) ; } } ) ; } } ) ; } ) ; } } function commonPath ( path1 , path2 ) { let commonPath = "" ; for ( let i = 0 ; i < path1 . length ; i ++ ) { if ( path1 . charAt ( i ) === path2 . charAt ( i ) ) { commonPath += path1 . charAt ( i ) ; } else { return commonPath ; } } return commonPath ; } function toCompilerOptions ( sbtOptions ) { const unparsedCompilerOptions = sbtOptions . tsconfig [ "compilerOptions" ] ; if ( unparsedCompilerOptions . outFile ) { const outFile = path . join ( target , unparsedCompilerOptions . outFile ) ; logger . debug ( "single outFile " , outFile ) ; unparsedCompilerOptions . outFile = outFile ; } if ( sbtOptions . assetsDirs . length == 2 ) { unparsedCompilerOptions . rootDirs = sbtOptions . assetsDirs ; } else if ( sbtOptions . assetsDirs . length == 1 ) { unparsedCompilerOptions . rootDir = sbtOptions . assetsDirs [ 0 ] ; } else { throw new Error ( "nr of asset dirs should always be 1 or 2" ) ; } unparsedCompilerOptions . listEmittedFiles = true ; return convertCompilerOptionsFromJson ( unparsedCompilerOptions , sbtOptions . tsconfigDir , "tsconfig.json" ) ; } function flatFilesWritten ( results ) { const files = [ ] ; results . forEach ( cfr => cfr . result . filesWritten . forEach ( fw => files . push ( fw ) ) ) ; return files ; } function isCodeFile ( f ) { return ! ( isDeclarationFile ( f ) ) ; } function isDeclarationFile ( f ) { const fileName = f . fileName ; return ".d.ts" === fileName . substring ( fileName . length - 5 ) ; } function flatten < T > ( xs ) { let result = [ ] ; xs . forEach ( x => { if ( x . value ) result . push ( x . value ) ; } ) ; return result ; } } function toCompilationResult ( sourceMappings , compilerOptions ) { return sourceFile => { return sourceMappings . find ( sourceFile . fileName ) . map ( ( sm ) => { let deps = [ sourceFile . fileName ] . concat ( sourceFile . referencedFiles . map ( f => f . fileName ) ) ; let outputFile = determineOutFile ( sm . toOutputPath ( compilerOptions . outDir ! , ".js" ) , compilerOptions ) ; let filesWritten = [ outputFile ] ; if ( compilerOptions . declaration ) { let outputFileDeclaration = sm . toOutputPath ( compilerOptions . outDir ! , ".d.ts" ) ; filesWritten . push ( outputFileDeclaration ) ; } if ( compilerOptions . sourceMap && ! compilerOptions . inlineSourceMap ) { let outputFileMap = outputFile + ".map" ; filesWritten . push ( outputFileMap ) ; } const result = < CompilationFileResult > { source : sourceFile . fileName , result : { filesRead : deps , filesWritten : filesWritten } } ; return result ; function determineOutFile ( outFile , options ) { if ( options . outFile ) { logger . debug ( "single outFile " , options . outFile ) ; return options . outFile ; } else { return outFile ; } } } ) ; } ; } function findPreemitProblems ( program , tsIgnoreList ? ) { let diagnostics = getPreEmitDiagnostics ( program ) ; if ( tsIgnoreList ) return diagnostics . filter ( ignoreDiagnostic ( tsIgnoreList ) ) . map ( parseDiagnostic ) ; else return diagnostics . map ( parseDiagnostic ) ; } function toProblems ( diagnostics , tsIgnoreList ? ) { if ( tsIgnoreList ) return diagnostics . filter ( ignoreDiagnostic ( tsIgnoreList ) ) . map ( parseDiagnostic ) ; else return diagnostics . map ( parseDiagnostic ) ; } function ignoreDiagnostic ( tsIgnoreList ) { return ( d ) => tsIgnoreList . indexOf ( d . code ) === - 1 ; } function parseDiagnostic ( d ) { let lineCol = { line : 0 , character : 0 } ; let fileName = "tsconfig.json" ; let lineText = "" ; if ( d . file && d . start ) { lineCol = d . file . getLineAndCharacterOfPosition ( d . start ) ; let lineStart = d . file . getLineStarts ( ) [ lineCol . line ] ; let lineEnd = d . file . getLineStarts ( ) [ lineCol . line + 1 ] ; lineText = d . file . text . substring ( lineStart , lineEnd ) ; fileName = d . file . fileName ; } let problem = < Problem > { lineNumber : lineCol . line + 1 , characterOffset : lineCol . character , message : "TS" + d . code + " " + flattenDiagnosticMessageText ( d . messageText , sys . newLine ) , source : fileName , severity : toSeverity ( d . category ) , lineContent : lineText } ; return problem ; function toSeverity ( i ) { if ( i === 0 ) { return "warn" ; } else if ( i === 1 ) { return "error" ; } else if ( i === 2 ) { return "info" ; } else { return "error" ; } } }