import db from "debug" ; import * as fs from "fs-extra" ; import * as path from "path" ; import { CustomError } from "ts-custom-error" ; import { trace , tracef } from "../utils/trace" ; import { VmModule } from "./context" ; import { ChainableHost , chainHosts , debugChainableHosts } from "./hosts" ; import { ModuleResolver } from "./modules" ; import * as ts from "./tsmod" ; const tsmod = ts . tsmod ; const debugCompile = false ; const debugPreprocessing = false ; let debugIntermediateDom = false ; if ( debugCompile || debugPreprocessing ) debugIntermediateDom = true ; const debugAction = db ( "adapt:compile:action" ) ; function compilerDefaults ( ) { return { target : tsmod ( ) . ScriptTarget . ES2016 , module : tsmod ( ) . ModuleKind . CommonJS , experimentalDecorators : true , inlineSourceMap : true , allowJs : true , jsx : tsmod ( ) . JsxEmit . React , jsxFactory : "Adapt.createElement" , resolveJsonModule : true , noErrorTruncation : true , esModuleInterop : true , } ; } export class CompileError extends CustomError { constructor ( public diags , msg ) { super ( msg ) ; } } class EmitError extends CustomError { constructor ( public filename , public diags , public emitSkipped ) { super ( ) ; } } function diagText ( diags , cwd , lineOffset ) { return diags . map ( ( d ) => { let msg = "" ; let src = "" ; if ( d . file ) { msg += path . relative ( cwd , d . file . fileName ) ; if ( d . start ) { const { line , character } = d . file . getLineAndCharacterOfPosition ( d . start ) ; msg += ` (${ line + 1 + lineOffset },${ character + 1 })` ; const lineStart = d . file . getPositionOfLineAndCharacter ( line , 0 ) ; const lineEnd = d . file . getLineEndOfPosition ( d . start ) ; src = "\n" + d . file . getFullText ( ) . substr ( lineStart , lineEnd - lineStart ) + "\n" ; src += " " . repeat ( character ) + "^\n" ; } msg += ": " ; } msg += tsmod ( ) . flattenDiagnosticMessageText ( d . messageText , "\n" ) ; msg += ` (${ d . code })` ; msg += src ; return msg ; } ) . join ( "\n" ) ; } interface ILangCache { version : string ; snapshot ? : ts . IScriptSnapshot ; sourceFile ? : ts . SourceFile ; contents ? : string ; } const cacheSourceFiles = false ; class DomCompileHost extends ChainableHost implements ts . LanguageServiceHost { private cache : Map < string , ILangCache > ; constructor ( public rootFiles , readonly compilerOptions , chainHost , id ? , public getProjectVersion ? ) { super ( chainHost . cwd ) ; this . cache = new Map < string , ILangCache > ( ) ; this . setSource ( chainHost ) ; if ( id ) this . _id = id ; } getScriptFileNames = ( ) => this . rootFiles ; getScriptVersion = ( filename ) => this . getFileVersion ( filename ) ; getCompilationSettings = ( ) => this . compilerOptions ; getDefaultLibFileName ( ) { return tsmod ( ) . getDefaultLibFilePath ( this . compilerOptions ) ; } cacheEntry ( fileName ) { const curVer = this . getScriptVersion ( fileName ) ; if ( curVer === undefined ) return undefined ; let c = this . cache . get ( fileName ) ; if ( c && ( c . version === curVer ) ) return c ; const contents = this . readFile ( fileName ) ; if ( contents === undefined ) return undefined ; c = { version : curVer , contents } ; this . cache . set ( fileName , c ) ; return c ; } @ tracef ( debugChainableHosts ) getScriptSnapshot ( fileName ) { fileName = this . getCanonicalFileName ( fileName ) ; const c = this . cacheEntry ( fileName ) ; if ( ! c ) return undefined ; if ( c . snapshot ) trace ( debugChainableHosts , `Cached snapshot: ${ fileName }` ) ; if ( c . snapshot ) return c . snapshot ; if ( ! c . contents ) throw new Error ( `Unable to create snapshot` ) ; trace ( debugChainableHosts , `New snapshot: ${ fileName }` ) ; c . snapshot = tsmod ( ) . ScriptSnapshot . fromString ( c . contents ) ; return c . snapshot ; } @ tracef ( debugChainableHosts ) getSourceFile ( fileName , languageVersion , onError ? ) { const c = this . cacheEntry ( fileName ) ; if ( ! c ) return undefined ; trace ( debugChainableHosts , `getSourceFile: ${ fileName }` ) ; if ( c . sourceFile ) return c . sourceFile ; c . sourceFile = this . source . getSourceFile ( fileName , languageVersion , onError ) ; if ( c . sourceFile !== undefined ) return c . sourceFile ; trace ( debugChainableHosts , `New sourceFile: ${ fileName }` ) ; if ( ! c . contents ) throw new Error ( `Unable to create source file` ) ; const sf = tsmod ( ) . createSourceFile ( fileName , c . contents , languageVersion , true ) ; if ( cacheSourceFiles ) c . sourceFile = sf ; return sf ; } dir ( ) { const ret = [ ] ; for ( const filename of this . cache . keys ( ) ) { const c = this . cache . get ( filename ) ; if ( ! c ) continue ; let info = "" ; if ( c . sourceFile ) info += "SF" ; if ( c . snapshot ) info += "SS" ; if ( c . contents !== undefined ) info += `C [${ c . contents . length }]` ; ret . push ( `${ filename }: ${ info }` ) ; } return ret ; } } export class Compiler { public service : ts . LanguageService ; private baseHost : ChainableHost ; private primaryChain : DomCompileHost ; private projectVersion = 0 ; private _rootFiles : string [ ] ; constructor ( projectRoot , rootFiles , chainHost , compilerOptions ? ) { const finalOptions = { ... compilerDefaults ( ) , ... compilerOptions } ; ( finalOptions as any ) . allowNonTsExtensions = true ; finalOptions . typeRoots = [ path . join ( projectRoot , "node_modules" , "@types" ) ] ; const verFunc = this . getProjectVersion . bind ( this ) ; this . _rootFiles = rootFiles ; this . baseHost = chainHost ; const partialChain = chainHosts ( new ModuleResolver ( finalOptions , undefined , "Prim" ) , chainHost ) ; this . primaryChain = new DomCompileHost ( rootFiles , finalOptions , partialChain , "Prim" , verFunc ) ; this . service = tsmod ( ) . createLanguageService ( this . primaryChain ) ; } get host ( ) : ChainableHost { return this . primaryChain ; } get rootFiles ( ) : string [ ] { return this . _rootFiles ; } set rootFiles ( val ) { this . _rootFiles = val ; this . primaryChain . rootFiles = val ; } @ tracef ( debugChainableHosts ) getProjectVersion ( ) { return this . projectVersion . toString ( ) ; } @ tracef ( debugChainableHosts ) compile ( code , filename , module , lineOffset = 0 ) { debugAction ( `Compile start: ${ filename }` ) ; this . registerExtension ( ".ts" , module ) ; this . registerExtension ( ".tsx" , module ) ; this . projectVersion ++ ; try { const ret = this . _compile ( code , filename ) ; debugAction ( `Compile done: ${ filename }` ) ; return ret ; } catch ( err ) { debugAction ( `Compile error: ${ filename }` ) ; if ( err instanceof EmitError ) { throw new CompileError ( err . diags , diagText ( err . diags , this . primaryChain . getCurrentDirectory ( ) , lineOffset ) ) ; } throw err ; } } registerExtension ( ext , mainModule ) { const compiler = this ; const chain = this . primaryChain ; mainModule . registerExt ( ext , function ( mod , filename ) { const oldcompile = mod . _compile ; mod . _compile = function ( code , fname ) { return oldcompile . call ( this , compiler . _compile ( code , fname ) , fname ) ; } ; try { return mod . _compile ( chain . readFile ( filename ) , filename ) ; } catch ( err ) { if ( err instanceof EmitError ) { throw new CompileError ( err . diags , diagText ( err . diags , compiler . primaryChain . getCurrentDirectory ( ) , 0 ) ) ; } throw err ; } } ) ; } dir ( ) { trace ( debugChainableHosts , `\n*** Primary chain ***` ) ; this . primaryChain . dirTrace ( ) ; } @ tracef ( debugChainableHosts || debugCompile ) private _compile ( _code , filename ) { let output ; const mkDebugWrite = ( enable = true ) => { if ( ! enable ) return ( ) => { return ; } ; const startPath = this . primaryChain . getCurrentDirectory ( ) ; const localOutputDir = path . join ( process . cwd ( ) , "DebugOut" ) ; const trim = startPath . length ; return function debugFileWrite ( filePath , contents ) { filePath = path . resolve ( startPath , filePath ) ; if ( ! filePath . startsWith ( startPath ) ) return ; const relPath = filePath . slice ( trim ) ; const outputDir = path . join ( localOutputDir , path . dirname ( relPath ) ) ; try { fs . statSync ( outputDir ) ; } catch ( err ) { fs . mkdirpSync ( outputDir ) ; } fs . writeFileSync ( path . join ( localOutputDir , relPath ) , contents ) ; } ; } ; try { debugAction ( `Emit output start: ${ filename }` ) ; output = this . service . getEmitOutput ( filename ) ; debugAction ( `Emit output done: ${ filename }` ) ; } catch ( err ) { trace ( debugChainableHosts , `Error compiling. Dumping chain info` ) ; if ( debugChainableHosts ) this . dir ( ) ; throw err ; } const diagnostics = this . service . getCompilerOptionsDiagnostics ( ) . concat ( this . service . getSyntacticDiagnostics ( filename ) ) . concat ( this . service . getSemanticDiagnostics ( filename ) ) ; if ( diagnostics . length || output . emitSkipped ) { throw new EmitError ( filename , diagnostics , output . emitSkipped ) ; } if ( /\.d\.ts$/ . test ( filename ) ) { return null ; } const debugWrite = mkDebugWrite ( debugIntermediateDom ) ; let jsOut = null ; for ( const out of output . outputFiles ) { if ( /\.js$/ . test ( out . name ) ) { jsOut = out ; } this . baseHost . writeFile ( out . name , out . text ) ; debugWrite ( out . name , out . text ) ; } if ( ! jsOut ) throw new Error ( `Couldn't find output file` ) ; return jsOut . text ; } }