namespace Playback { interface FileInformation { contents ? : string ; contentsPath ? : string ; codepage : number ; bom ? : string ; } interface FindFileResult { } interface IoLogFile { path : string ; codepage : number ; result ? : FileInformation ; } export interface IoLog { timestamp : string ; arguments : string [ ] ; executingPath : string ; currentDirectory : string ; useCustomLibraryFile ? : boolean ; filesRead : IoLogFile [ ] ; filesWritten : { path : string ; contents ? : string ; contentsPath ? : string ; bom : boolean ; } [ ] ; filesDeleted : string [ ] ; filesAppended : { path : string ; contents ? : string ; contentsPath ? : string ; } [ ] ; fileExists : { path : string ; result ? : boolean ; } [ ] ; filesFound : { path : string ; pattern : string ; result ? : FindFileResult ; } [ ] ; dirs : { path : string ; re : string ; re_m : boolean ; re_g : boolean ; re_i : boolean ; opts : { recursive ? : boolean ; } ; result ? : string [ ] ; } [ ] ; dirExists : { path : string ; result ? : boolean ; } [ ] ; dirsCreated : string [ ] ; pathsResolved : { path : string ; result ? : string ; } [ ] ; directoriesRead : { path : string ; extensions : readonly string [ ] | undefined ; exclude : readonly string [ ] | undefined ; include : readonly string [ ] | undefined ; depth : number | undefined ; result : readonly string [ ] ; } [ ] ; useCaseSensitiveFileNames ? : boolean ; } interface PlaybackControl { startReplayFromFile ( logFileName ) : void ; startReplayFromString ( logContents ) : void ; startReplayFromData ( log ) : void ; endReplay ( ) : void ; startRecord ( logFileName ) : void ; endRecord ( ) : void ; } let recordLog ; let replayLog ; let replayFilesRead ; let recordLogFileNameBase = "" ; interface Memoized < T > { ( s ) : T ; reset ( ) : void ; } function memoize < T > ( func ) { let lookup = { } ; const run = ( ( s ) => { if ( lookup . hasOwnProperty ( s ) ) return lookup [ s ] ; return lookup [ s ] = func ( s ) ; } ) as Memoized < T > ; run . reset = ( ) => { lookup = undefined ! ; } ; return run ; } export interface PlaybackIO extends Harness . IO , PlaybackControl { } export interface PlaybackSystem extends ts . System , PlaybackControl { } function createEmptyLog ( ) { return { timestamp : ( new Date ( ) ) . toString ( ) , arguments : [ ] , currentDirectory : "" , filesRead : [ ] , directoriesRead : [ ] , filesWritten : [ ] , filesDeleted : [ ] , filesAppended : [ ] , fileExists : [ ] , filesFound : [ ] , dirs : [ ] , dirExists : [ ] , dirsCreated : [ ] , pathsResolved : [ ] , executingPath : "" } ; } export function newStyleLogIntoOldStyleLog ( log , host , baseName ) { for ( const file of log . filesAppended ) { if ( file . contentsPath ) { file . contents = host . readFile ( ts . combinePaths ( baseName , file . contentsPath ) ) ; delete file . contentsPath ; } } for ( const file of log . filesWritten ) { if ( file . contentsPath ) { file . contents = host . readFile ( ts . combinePaths ( baseName , file . contentsPath ) ) ; delete file . contentsPath ; } } for ( const file of log . filesRead ) { const result = file . result ! ; if ( result . contentsPath ) { result . contents = ( result . bom || "" ) + host . readFile ( ts . combinePaths ( baseName , result . contentsPath ) ) ; delete result . contentsPath ; } } return log ; } const canonicalizeForHarness = ts . createGetCanonicalFileName ( false ) ; function sanitizeTestFilePath ( name ) { const path = ts . toPath ( ts . normalizeSlashes ( name . replace ( /[\^<>:"|?*%]/g , "_" ) ) . replace ( /\.\.\//g , "__dotdot/" ) , "" , canonicalizeForHarness ) ; if ( ts . startsWith ( path , "/" ) ) { return path . substring ( 1 ) ; } return path ; } export function oldStyleLogIntoNewStyleLog ( log , writeFile , baseTestName ) { if ( log . filesAppended ) { for ( const file of log . filesAppended ) { if ( file . contents !== undefined ) { file . contentsPath = ts . combinePaths ( "appended" , sanitizeTestFilePath ( file . path ) ) ; writeFile ( ts . combinePaths ( baseTestName , file . contentsPath ) , file . contents ) ; delete file . contents ; } } } if ( log . filesWritten ) { for ( const file of log . filesWritten ) { if ( file . contents !== undefined ) { file . contentsPath = ts . combinePaths ( "written" , sanitizeTestFilePath ( file . path ) ) ; writeFile ( ts . combinePaths ( baseTestName , file . contentsPath ) , file . contents ) ; delete file . contents ; } } } if ( log . filesRead ) { for ( const file of log . filesRead ) { const result = file . result ! ; const { contents } = result ; if ( contents !== undefined ) { result . contentsPath = ts . combinePaths ( "read" , sanitizeTestFilePath ( file . path ) ) ; writeFile ( ts . combinePaths ( baseTestName , result . contentsPath ) , contents ) ; const len = contents . length ; if ( len >= 2 && contents . charCodeAt ( 0 ) === 0xfeff ) { result . bom = "\ufeff" ; } if ( len >= 2 && contents . charCodeAt ( 0 ) === 0xfffe ) { result . bom = "\ufffe" ; } if ( len >= 3 && contents . charCodeAt ( 0 ) === 0xefbb && contents . charCodeAt ( 1 ) === 0xbf ) { result . bom = "\uefbb\xbf" ; } delete result . contents ; } } } return log ; } export function initWrapper ( ... [ wrapper , underlying ] ) { ts . forEach ( Object . keys ( underlying ) , prop => { ( wrapper as any ) [ prop ] = ( underlying as any ) [ prop ] ; } ) ; wrapper . startReplayFromString = logString => { wrapper . startReplayFromData ( JSON . parse ( logString ) ) ; } ; wrapper . startReplayFromData = log => { replayLog = log ; replayLog . filesRead = replayLog . filesRead . filter ( f => f . result ! . contents !== undefined ) ; replayFilesRead = new ts . Map ( ) ; for ( const file of replayLog . filesRead ) { replayFilesRead . set ( ts . normalizeSlashes ( file . path ) . toLowerCase ( ) , file ) ; } } ; wrapper . endReplay = ( ) => { replayLog = undefined ; replayFilesRead = undefined ; } ; wrapper . startRecord = ( fileNameBase ) => { recordLogFileNameBase = fileNameBase ; recordLog = createEmptyLog ( ) ; recordLog . useCaseSensitiveFileNames = typeof underlying . useCaseSensitiveFileNames === "function" ? underlying . useCaseSensitiveFileNames ( ) : underlying . useCaseSensitiveFileNames ; if ( typeof underlying . args !== "function" ) { recordLog . arguments = underlying . args ; } } ; wrapper . startReplayFromFile = logFn => { wrapper . startReplayFromString ( underlying . readFile ( logFn ) ! ) ; } ; wrapper . endRecord = ( ) => { if ( recordLog !== undefined ) { let i = 0 ; const getBase = ( ) => recordLogFileNameBase + i ; while ( underlying . fileExists ( ts . combinePaths ( getBase ( ) , "test.json" ) ) ) i ++ ; const newLog = oldStyleLogIntoNewStyleLog ( recordLog , ( path , str ) => underlying . writeFile ( path , str ) , getBase ( ) ) ; underlying . writeFile ( ts . combinePaths ( getBase ( ) , "test.json" ) , JSON . stringify ( newLog , null , 4 ) ) ; const syntheticTsconfig = generateTsconfig ( newLog ) ; if ( syntheticTsconfig ) { underlying . writeFile ( ts . combinePaths ( getBase ( ) , "tsconfig.json" ) , JSON . stringify ( syntheticTsconfig , null , 4 ) ) ; } recordLog = undefined ; } } ; function generateTsconfig ( newLog ) { if ( newLog . filesRead . some ( file => /tsconfig.+json$/ . test ( file . path ) ) ) { return ; } const files = [ ] ; for ( const file of newLog . filesRead ) { const result = file . result ! ; if ( result . contentsPath && Harness . isDefaultLibraryFile ( result . contentsPath ) && /\.[tj]s$/ . test ( result . contentsPath ) ) { files . push ( result . contentsPath ) ; } } return { compilerOptions : ts . parseCommandLine ( newLog . arguments ) . options , files } ; } wrapper . fileExists = recordReplay ( wrapper . fileExists , underlying ) ( path => callAndRecord ( underlying . fileExists ( path ) , recordLog ! . fileExists , { path } ) , memoize ( path => { if ( findFileByPath ( path , false ) ) { return true ; } else { return findResultByFields ( replayLog ! . fileExists , { path } , false ) ! ; } } ) ) ; wrapper . getExecutingFilePath = ( ) => { if ( replayLog !== undefined ) { return replayLog . executingPath ; } else if ( recordLog !== undefined ) { return recordLog . executingPath = underlying . getExecutingFilePath ( ) ; } else { return underlying . getExecutingFilePath ( ) ; } } ; wrapper . getCurrentDirectory = ( ) => { if ( replayLog !== undefined ) { return replayLog . currentDirectory || "" ; } else if ( recordLog !== undefined ) { return recordLog . currentDirectory = underlying . getCurrentDirectory ( ) ; } else { return underlying . getCurrentDirectory ( ) ; } } ; wrapper . resolvePath = recordReplay ( wrapper . resolvePath , underlying ) ( path => callAndRecord ( underlying . resolvePath ( path ) , recordLog ! . pathsResolved , { path } ) , memoize ( path => findResultByFields ( replayLog ! . pathsResolved , { path } , ! ts . isRootedDiskPath ( ts . normalizeSlashes ( path ) ) && replayLog ! . currentDirectory ? replayLog ! . currentDirectory + "/" + path : ts . normalizeSlashes ( path ) ) ) ) ; wrapper . readFile = recordReplay ( wrapper . readFile , underlying ) ( ( path ) => { const result = underlying . readFile ( path ) ; const logEntry = { path , codepage : 0 , result : { contents : result , codepage : 0 } } ; recordLog ! . filesRead . push ( logEntry ) ; return result ; } , memoize ( path => findFileByPath ( path , true ) ! . contents ) ) ; wrapper . readDirectory = recordReplay ( wrapper . readDirectory , underlying ) ( ( path , extensions , exclude , include , depth ) => { const result = ( underlying as ts . System ) . readDirectory ( path , extensions , exclude , include , depth ) ; recordLog ! . directoriesRead . push ( { path , extensions , exclude , include , depth , result } ) ; return result ; } , path => { const normalizedPath = ts . normalizePath ( path ) . toLowerCase ( ) ; return ts . flatMap ( replayLog ! . directoriesRead , directory => { if ( ts . normalizeSlashes ( directory . path ) . toLowerCase ( ) === normalizedPath ) { return directory . result ; } } ) ; } ) ; wrapper . writeFile = recordReplay ( wrapper . writeFile , underlying ) ( ( path , contents ) => callAndRecord ( underlying . writeFile ( path , contents ) , recordLog ! . filesWritten , { path , contents , bom : false } ) , ( ) => noOpReplay ( "writeFile" ) ) ; wrapper . exit = ( exitCode ) => { if ( recordLog !== undefined ) { wrapper . endRecord ( ) ; } underlying . exit ( exitCode ) ; } ; wrapper . useCaseSensitiveFileNames = ( ) => { if ( replayLog !== undefined ) { return ! ! replayLog . useCaseSensitiveFileNames ; } return typeof underlying . useCaseSensitiveFileNames === "function" ? underlying . useCaseSensitiveFileNames ( ) : underlying . useCaseSensitiveFileNames ; } ; } function recordReplay < T extends ts . AnyFunction > ( original , underlying ) { function createWrapper ( record , replay ) { return ( function ( ) { if ( replayLog !== undefined ) { return replay . apply ( undefined , arguments ) ; } else if ( recordLog !== undefined ) { return record . apply ( undefined , arguments ) ; } else { return original . apply ( underlying , arguments ) ; } } as any ) ; } return createWrapper ; } function callAndRecord < T , U > ( underlyingResult , logArray , logEntry ) { if ( underlyingResult !== undefined ) { ( logEntry as any ) . result = underlyingResult ; } logArray . push ( logEntry ) ; return underlyingResult ; } function findResultByFields < T > ( logArray , expectedFields , defaultValue ? ) { const predicate = ( entry ) => { return Object . getOwnPropertyNames ( expectedFields ) . every ( ( name ) => ( entry as any ) [ name ] === ( expectedFields as any ) [ name ] ) ; } ; const results = logArray . filter ( entry => predicate ( entry ) ) ; if ( results . length === 0 ) { if ( defaultValue !== undefined ) { return defaultValue ; } else { throw new Error ( "No matching result in log array for: " + JSON . stringify ( expectedFields ) ) ; } } return results [ 0 ] . result ; } function findFileByPath ( expectedPath , throwFileNotFoundError ) { const normalizedName = ts . normalizePath ( expectedPath ) . toLowerCase ( ) ; const result = replayFilesRead ! . get ( normalizedName ) ; if ( result ) { return result . result ; } if ( throwFileNotFoundError ) { throw new Error ( "No matching result in log array for path: " + expectedPath ) ; } else { return undefined ; } } function noOpReplay ( _name ) { } export function wrapIO ( underlying ) { const wrapper = { } as any ; initWrapper ( wrapper , underlying ) ; wrapper . directoryName = notSupported ; wrapper . createDirectory = notSupported ; wrapper . directoryExists = notSupported ; wrapper . deleteFile = notSupported ; wrapper . listFiles = notSupported ; return wrapper ; function notSupported ( ) { throw new Error ( "NotSupported" ) ; } } export function wrapSystem ( underlying ) { const wrapper = { } as any ; initWrapper ( wrapper , underlying ) ; return wrapper ; } } namespace ts . server { } namespace Harness { }