import * as fs from "fs" ; import { dirname , resolve , sep } from "path" ; import { InternalError } from "../error" ; import { trace , tracef } from "../utils" ; import * as ts from "./tsmod" ; const typeName = require ( "type-name" ) ; const tsmod = ts . tsmod ; export let debugChainableHosts = false ; export let debugChainableHostsVerbose = false ; if ( debugChainableHostsVerbose ) debugChainableHosts = true ; export let debugDir = true ; type FunctionPropertyNames < T > = { [ K in keyof T ] : T [ K ] extends Function ? K : never ; } [ keyof T ] ; type InitCanonicalize < T > = { [ K in keyof T ] ? : number ; } ; const initCanonicalize = { fileExists : 0 , directoryExists : 0 , readFile : 0 , getFileVersion : 0 , getSourceFile : 0 , writeFile : 0 , getDirectories : 0 , realFilename : 0 , } ; const noop = undefined ; function callSource ( proto , propKey , desc ) { return { ... desc , value : function _toSource ( this , ... args ) { if ( ! this . source ) throw new InternalError ( `source for ${ this . constructor . name } is null` ) ; return ( this . source as any ) [ propKey ] . apply ( this . source , args ) ; } } ; } export abstract class ChainableHost implements ts . CompilerHost { public _id = "" ; source : ChainableHost = null ; constructor ( readonly cwd ) { for ( const key of Object . keys ( initCanonicalize ) ) { this . canonicalizeFunc ( key , initCanonicalize [ key ] ) ; } } @ tracef ( debugChainableHosts ) @ callSource directoryExists ( directoryname ) { return noop ; } @ tracef ( debugChainableHosts ) @ callSource fileExists ( fileName ) { return noop ; } @ callSource getCancellationToken ( ) { return noop ; } @ callSource getDefaultLibFileName ( options ) { return noop ; } @ tracef ( debugChainableHosts ) @ callSource getDirectories ( path ) { return noop ; } @ tracef ( debugChainableHosts ) @ callSource getFileVersion ( fileName ) { return noop ; } @ tracef ( debugChainableHosts ) @ callSource getNewLine ( ) { return noop ; } @ tracef ( debugChainableHosts ) @ callSource getSourceFile ( fileName , languageVersion , onError ? ) { return noop ; } @ tracef ( debugChainableHosts ) @ callSource readFile ( fileName ) { return noop ; } /**
     * Should be implemented by any Host that performs filename translation.
     * Given a possibly "faked" or virtual filename, return the real filename
     * that corresponds.
     */ @ callSource realFilename ( fileName ) { return noop ; } @ callSource resolveModuleName ( modName , containingFile , runnable ? ) { return noop ; } @ callSource resolveModuleNames ( moduleNames , containingFile , reusedNames ? ) { return noop ; } @ callSource useCaseSensitiveFileNames ( ) { return noop ; } @ tracef ( debugChainableHosts ) @ callSource writeFile ( fileName , data ) { return noop ; } getCanonicalFileName ( fileName ) { return resolve ( this . getCurrentDirectory ( ) , fileName ) ; } getCurrentDirectory ( ) { return ( this . source && this . source . getCurrentDirectory ( ) ) || this . cwd ; } setSource ( source ) { if ( this . source === null ) { this . source = source ; } else { throw new Error ( `A chainable host can be connected to a source ` + `only once. It looks like you're trying to include the same ` + `instance in multiple chains.` ) ; } } dir ( ) { return [ ] ; } dirTrace ( ) { const dflag = debugChainableHosts || debugDir ; if ( ! dflag ) return ; let s = this ; while ( true ) { const objname = typeName ( s ) + s . _id ; trace ( dflag , `\nFiles in ${ objname }:` ) ; for ( const f of s . dir ( ) ) { trace ( dflag , `  ${ f }` ) ; } if ( ! s . source ) break ; s = s . source ; } } private canonicalizeFunc ( funcName , argNo ) { const origFunc = this [ funcName ] ; ( this as any ) [ funcName ] = function ( this , ... args ) { args [ argNo ] = resolve ( this . getCurrentDirectory ( ) , args [ argNo ] ) ; return ( origFunc as any ) . apply ( this , args ) ; } ; } } export class HostFinal extends ChainableHost { fileExists ( ) { return false ; } directoryExists ( ) { return false ; } getFileVersion ( ) { return undefined as any ; } getSourceFile ( ) { return undefined ; } useCaseSensitiveFileNames ( ) { return true ; } getNewLine ( ) { return "\n" ; } getDefaultLibFileName ( ) { return "lib.d.ts" ; } resolveModuleName ( ) { return { resolvedFileName : undefined } as any ; } getDirectories ( ) { return [ ] ; } readFile ( ) { return undefined ; } getCancellationToken ( ) { return null as any ; } resolveModuleNames ( moduleNames , containingFile ) { return moduleNames . map ( ( modName ) => { return this . resolveModuleName ( ) ; } ) ; } realFilename ( ) { return undefined ; } writeFile ( ) { throw new Error ( `Base Compiler host is not writable` ) ; } dir ( ) { return [ ] ; } } export class FileSystemHost extends ChainableHost { constructor ( private rootDir , cwd = process . cwd ( ) ) { super ( cwd ) ; this . rootDir = fs . realpathSync ( rootDir ) ; } readDirectory ( path , extensions ? , excludes ? , includes ? , depth ? ) { if ( ! this . allowed ( path ) ) return [ ] ; return tsmod ( ) . sys . readDirectory ( path , extensions , excludes , includes , depth ) ; } getDirectories ( fileName ) { if ( ! this . allowed ( fileName ) ) return [ ] ; return tsmod ( ) . sys . getDirectories ( fileName ) ; } directoryExists ( path ) { if ( ! this . allowed ( path ) ) return false ; return tsmod ( ) . sys . directoryExists ( path ) ; } @ tracef ( debugChainableHosts ) readFile ( path , encoding ? ) { if ( ! this . allowed ( path ) ) return undefined ; const contents = fs . readFileSync ( path , encoding ) ; if ( contents ) return contents . toString ( ) ; return undefined ; } @ tracef ( debugChainableHosts ) getFileVersion ( fileName ) { try { const stats = fs . statSync ( fileName ) ; return stats . mtimeMs . toString ( ) ; } catch ( err ) { return this . source . getFileVersion ( fileName ) ; } } @ tracef ( debugChainableHosts ) fileExists ( path ) { return this . allowed ( path ) && tsmod ( ) . sys . fileExists ( path ) ; } realFilename ( fileName ) { return fileName ; } writeFile ( ) { throw new Error ( `FileSystemHost is not writable` ) ; } dir ( ) { return [ `File system at ${ this . rootDir }` ] ; } private allowed ( path ) { try { const resolved = fs . realpathSync ( path ) ; return resolved . startsWith ( this . rootDir ) ; } catch ( err ) { if ( err . code && err . code === "ENOENT" ) { return false ; } throw err ; } } } interface MemFileVersion { version : number ; contents : string ; } export class MemoryHost extends ChainableHost { private files = new Map < string , MemFileVersion > ( ) ; private dirs = new Map < string , Set < string > > ( ) ; constructor ( rootDir , cwd = process . cwd ( ) ) { super ( cwd ) ; this . mkdirs ( rootDir ) ; this . mkdirs ( cwd ) ; } @ tracef ( debugChainableHosts ) writeFile ( path , data ) { let f = this . files . get ( path ) ; if ( f !== undefined ) { f . version ++ ; f . contents = data ; } else { f = { version : 1 , contents : data } ; this . files . set ( path , f ) ; const dir = dirname ( path ) ; this . mkdirs ( dir ) ; const dirSet = this . dirs . get ( dir ) ; if ( ! dirSet ) throw new InternalError ( `dir not found` ) ; dirSet . add ( path ) ; } } @ tracef ( debugChainableHosts ) readFile ( path , encoding ? ) { const f = this . files . get ( path ) ; if ( f !== undefined ) { return f . contents ; } return this . source . readFile ( path ) ; } @ tracef ( debugChainableHosts ) getFileVersion ( fileName ) { const f = this . files . get ( fileName ) ; if ( f !== undefined ) { return f . version . toString ( ) ; } return this . source . getFileVersion ( fileName ) ; } @ tracef ( debugChainableHosts ) fileExists ( path ) { return ( this . files . has ( path ) || super . fileExists ( path ) ) ; } @ tracef ( debugChainableHosts ) directoryExists ( directoryName ) { return this . dirs . has ( directoryName ) || this . source . directoryExists ( directoryName ) ; } @ tracef ( debugChainableHosts ) getDirectories ( path ) { const pdirs = this . source . getDirectories ( path ) ; if ( ! path . endsWith ( sep ) ) { path += sep ; } const re = new RegExp ( `^${ path }[^/]+$` ) ; for ( const d of this . dirs . keys ( ) ) { if ( re . test ( d ) ) pdirs . push ( d ) ; } return pdirs ; } dir ( ) { return Array . from ( this . files . keys ( ) ) ; } private mkdirs ( dir ) { while ( true ) { if ( ! this . dirs . get ( dir ) ) { this . dirs . set ( dir , new Set < string > ( ) ) ; } const parent = dirname ( dir ) ; if ( parent === dir ) { break ; } dir = parent ; } } } export function chainHosts ( ... hosts ) { if ( hosts . length < 2 ) throw new Error ( `Must chain at least two hosts` ) ; for ( let i = 1 ; i < hosts . length ; i ++ ) { hosts [ i - 1 ] . setSource ( hosts [ i ] ) ; } return hosts [ 0 ] ; } export function MemFileHost ( rootDir , cwd = process . cwd ( ) ) { return chainHosts ( new MemoryHost ( rootDir , cwd ) , new FileSystemHost ( rootDir , cwd ) , new HostFinal ( cwd ) ) ; }