import { findPackageDirs } from "@adpt/utils" ; import * as path from "path" ; import * as vm from "vm" ; const Module = require ( "module" ) ; import * as ld from "lodash" ; import { InternalError , isError , ProjectCompileError , ProjectRunError , ThrewNonError } from "../error" ; import { trace , tracef } from "../utils" ; import { CompileError } from "./compile" ; import { ChainableHost } from "./hosts" ; const builtInModules = new Set < string > ( Module . builtinModules ) ; const debugVm = false ; const packageDirs = findPackageDirs ( __dirname ) ; const tsStackExclude = RegExp ( "\n.*?\\(" + packageDirs . root + "(?:/dist)?/src/ts/.*?$" , "mg" ) ; const ctxStackEnd = /\n[^\n]*Script\.runInContext(?:.|\n)*/ ; function getProjectStack ( projectError ) { if ( projectError . stack ) { let ctxStack = projectError . stack . replace ( ctxStackEnd , "" ) ; ctxStack = ctxStack . replace ( tsStackExclude , "" ) ; return ctxStack ; } return "[No stack]" ; } export interface Extensions { [ ext ] : any ; } export interface RequireCache { [ abspath ] : NodeModule ; } interface Modules { [ abspath ] : VmModule ; } function isRelative ( loc ) { return loc . startsWith ( "./" ) || loc . startsWith ( "../" ) ; } export class VmModule { extensions : Extensions ; requireCache : RequireCache ; _compile = this . runJs ; ctxModule : NodeModule ; vmModules : Modules ; private hostModCache : any ; constructor ( public id , private vmContext , public host , public parent ? ) { if ( parent ) { this . extensions = parent . extensions ; this . requireCache = parent . requireCache ; this . hostModCache = parent . hostModCache ; this . vmModules = parent . vmModules ; } else { this . extensions = Object . create ( null ) ; this . requireCache = Object . create ( null ) ; this . hostModCache = Object . create ( null ) ; this . vmModules = Object . create ( null ) ; this . extensions [ ".js" ] = this . runJsModule . bind ( this ) ; this . extensions [ ".json" ] = this . runJsonModule . bind ( this ) ; } this . ctxModule = new Module ( id , ( parent && parent . ctxModule ) || null ) ; this . ctxModule . filename = id ; } param ctx initMain ( ctx ) { if ( this . parent ) throw new InternalError ( `initMain should only be called on top-level VmModule` ) ; this . vmContext = ctx ; this . loadHostMod ( "callsites" ) ; } @ tracef ( debugVm ) requireResolve ( request , options ? ) { if ( options ) { throw new Error ( "require.resolve options not supported yet." ) ; } const resolved = this . host . resolveModuleName ( request , this . id , true ) ; if ( resolved ) return resolved . resolvedFileName ; if ( isRelative ( request ) ) request = path . join ( path . dirname ( this . id ) , request ) ; if ( ! path . isAbsolute ( request ) ) { const err = new Error ( `Cannot find module '${ request }'` ) ; ( err as any ) . code = "MODULE_NOT_FOUND" ; throw err ; } return require . resolve ( request , options ) ; } @ tracef ( debugVm ) require ( modName ) { let hostMod = this . requireHostMod ( modName ) ; if ( hostMod !== undefined ) return hostMod ; if ( builtInModules . has ( modName ) ) { hostMod = this . requireBuiltin ( modName ) ; if ( hostMod === undefined ) throw new InternalError ( `Cannot find module '${ modName }'` ) ; return hostMod ; } let resolved ; try { resolved = this . requireResolve ( modName ) ; } catch ( e ) { if ( ! ld . isError ( e ) ) throw e ; if ( ! e . message . startsWith ( "Cannot find" ) ) throw e ; } if ( resolved ) { const resolvedPath = resolved ; const cached = this . requireCache [ resolvedPath ] ; if ( cached ) return cached . exports ; const newMod = new VmModule ( resolvedPath , this . vmContext , this . host , this ) ; this . vmModules [ resolvedPath ] = newMod ; this . requireCache [ resolvedPath ] = newMod . ctxModule ; const ext = path . extname ( resolvedPath ) || ".js" ; this . extensions [ ext ] ( newMod , resolvedPath ) ; newMod . ctxModule . loaded = true ; return newMod . ctxModule . exports ; } throw new Error ( `Unable to find module ${ modName } ` + `imported from ${ this . id }` ) ; } @ tracef ( debugVm ) registerExt ( ext , func ) { this . extensions [ ext ] = func ; } private loadHostMod ( modName ) { const cached = this . hostModCache [ modName ] ; if ( cached !== undefined ) return cached ; const mod = require ( modName ) ; this . hostModCache [ modName ] = mod ; return mod ; } @ tracef ( debugVm ) private requireHostMod ( modName ) { return this . hostModCache [ modName ] ; } @ tracef ( debugVm ) private requireBuiltin ( modName ) { try { return this . loadHostMod ( modName ) ; } catch ( e ) { if ( e . code === "MODULE_NOT_FOUND" ) return undefined ; throw e ; } } @ tracef ( debugVm ) private runJsonModule ( mod , filename ) { const contents = this . host . readFile ( filename ) ; if ( contents == null ) { throw new Error ( `Unable to find file contents for ${ filename }` ) ; } try { mod . ctxModule . exports = JSON . parse ( contents ) ; } catch ( err ) { err . message = filename + ": " + err . message ; throw err ; } } @ tracef ( debugVm ) private runJsModule ( mod , filename ) { const contents = this . host . readFile ( filename ) ; if ( contents == null ) { throw new Error ( `Unable to find file contents for ${ filename }` ) ; } return mod . runJs ( contents , filename ) ; } @ tracef ( debugVm ) private runJs ( content , filename ) { if ( ! this . vmContext ) throw new Error ( `vmContext is not set` ) ; const wrapper = Module . wrap ( content ) ; const compiled = vm . runInContext ( wrapper , this . vmContext , { filename } ) ; const require = ( ( ) => { const ret = this . require . bind ( this ) as NodeRequire ; ret . resolve = this . requireResolve . bind ( this ) ; ret . cache = this . requireCache ; return ret ; } ) ( ) ; const dirname = path . dirname ( filename ) ; try { return compiled . call ( this . ctxModule . exports , this . ctxModule . exports , require , this . ctxModule , filename , dirname ) ; } catch ( err ) { if ( ( err instanceof ProjectRunError ) || ( err instanceof CompileError ) ) { throw err ; } if ( ! isError ( err ) ) err = new ThrewNonError ( err ) ; throw new ProjectRunError ( err , getProjectStack ( err ) , err . stack ) ; } } } const hostGlobals = ( ) => ( { version : parseInt ( process . versions . node . split ( "." ) [ 0 ] , 10 ) , process , console , setTimeout , setInterval , setImmediate , clearTimeout , clearInterval , clearImmediate , Buffer , } ) ; export class VmContext { mainModule : VmModule ; constructor ( public vmGlobal , dirname , public filename , public host ) { vmGlobal . __filename = filename ; vmGlobal . __dirname = dirname ; const module = new VmModule ( filename , undefined , host , undefined ) ; this . mainModule = module ; vmGlobal . exports = module . ctxModule . exports ; vmGlobal . module = module . ctxModule ; vmGlobal . require = module . require . bind ( module ) ; vmGlobal . global = vmGlobal ; const hGlobals = hostGlobals ( ) ; for ( const prop of Object . keys ( hGlobals ) ) { vmGlobal [ prop ] = ( hGlobals as any ) [ prop ] ; } vm . createContext ( vmGlobal ) ; module . initMain ( vmGlobal ) ; } @ tracef ( debugVm ) run ( jsText ) { let val ; try { val = vm . runInContext ( jsText , this . vmGlobal , { filename : this . filename } ) ; } catch ( err ) { if ( err instanceof CompileError ) { err = new ProjectCompileError ( err . message ) ; } if ( ! isError ( err ) ) err = new ThrewNonError ( err ) ; if ( ! ( err instanceof ProjectRunError ) ) { err = new ProjectRunError ( err , getProjectStack ( err ) , err . stack ) ; } throw err ; } if ( debugVm ) { trace ( debugVm , `RESULT: ${ JSON . stringify ( val , null , 2 ) }` ) ; } return val ; } }