import * as vm from "vm" ; import * as fs from "fs-extra" ; import * as path from "path" ; import * as console from "console" ; import { PluginManager } from "./PluginManager" ; import { IPluginInfo } from "./PluginInfo" ; import Debug from "debug" ; import { PluginSandbox } from "../index" ; const debug = Debug ( "live-plugin-manager.PluginVm" ) ; const SCOPED_REGEX = /^(@[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)(.*)/ ; export class PluginVm { private requireCache = new Map < IPluginInfo , Map < string , NodeModule > > ( ) ; private sandboxCache = new Map < IPluginInfo , NodeJS . Global > ( ) ; constructor ( private readonly manager ) { } unload ( pluginContext ) { this . requireCache . delete ( pluginContext ) ; this . sandboxCache . delete ( pluginContext ) ; } load ( pluginContext , filePath ) { let moduleInstance = this . getCache ( pluginContext , filePath ) ; if ( moduleInstance ) { if ( debug . enabled ) { debug ( `${ filePath } loaded from cache` ) ; } return moduleInstance . exports ; } if ( debug . enabled ) { debug ( `Loading ${ filePath } ...` ) ; } const sandbox = this . createModuleSandbox ( pluginContext , filePath ) ; moduleInstance = sandbox . module ; const filePathExtension = path . extname ( filePath ) . toLowerCase ( ) ; if ( filePathExtension === ".js" ) { const code = fs . readFileSync ( filePath , "utf8" ) ; this . setCache ( pluginContext , filePath , moduleInstance ) ; try { this . vmRunScriptInSandbox ( sandbox , filePath , code ) ; } catch ( e ) { this . removeCache ( pluginContext , filePath ) ; throw e ; } } else if ( filePathExtension === ".json" ) { sandbox . module . exports = fs . readJsonSync ( filePath ) ; this . setCache ( pluginContext , filePath , moduleInstance ) ; } else { throw new Error ( "Invalid javascript file " + filePath ) ; } moduleInstance . loaded = true ; return moduleInstance . exports ; } resolve ( pluginContext , filePath ) { return this . sandboxResolve ( pluginContext , pluginContext . location , filePath ) ; } runScript ( code ) { const name = "dynamic-" + Date . now ; const filePath = path . join ( this . manager . options . pluginsPath , name + ".js" ) ; const pluginContext = { location : path . join ( this . manager . options . pluginsPath , name ) , mainFile : filePath , name , version : "1.0.0" , dependencies : { } } ; try { return this . vmRunScriptInPlugin ( pluginContext , filePath , code ) ; } finally { this . unload ( pluginContext ) ; } } splitRequire ( fullName ) { const scopedInfo = this . getScopedInfo ( fullName ) ; if ( scopedInfo ) { return scopedInfo ; } const slashPosition = fullName . indexOf ( "/" ) ; let requiredPath ; let pluginName = fullName ; if ( slashPosition > 0 ) { pluginName = fullName . substring ( 0 , slashPosition ) ; requiredPath = "." + fullName . substring ( slashPosition ) ; } return { pluginName , requiredPath } ; } private getScopedInfo ( fullName ) { const match = SCOPED_REGEX . exec ( fullName ) ; if ( ! match ) { return undefined ; } const requiredPath = match [ 2 ] ? "." + match [ 2 ] : undefined ; return { pluginName : match [ 1 ] , requiredPath } ; } private vmRunScriptInSandbox ( moduleSandbox , filePath , code ) { const moduleContext = vm . createContext ( moduleSandbox ) ; const iifeCode = `
			(function(exports){
				${ code }
			}(module.exports));` ; const vmOptions = { displayErrors : true , filename : filePath } ; const script = new vm . Script ( iifeCode , vmOptions ) ; script . runInContext ( moduleContext , vmOptions ) ; } private vmRunScriptInPlugin ( pluginContext , filePath , code ) { const sandbox = this . createModuleSandbox ( pluginContext , filePath ) ; this . vmRunScriptInSandbox ( sandbox , filePath , code ) ; sandbox . module . loaded = true ; return sandbox . module . exports ; } private getCache ( pluginContext , filePath ) { const moduleCache = this . requireCache . get ( pluginContext ) ; if ( ! moduleCache ) { return undefined ; } return moduleCache . get ( filePath ) ; } private setCache ( pluginContext , filePath , instance ) { let moduleCache = this . requireCache . get ( pluginContext ) ; if ( ! moduleCache ) { moduleCache = new Map < string , any > ( ) ; this . requireCache . set ( pluginContext , moduleCache ) ; } moduleCache . set ( filePath , instance ) ; } private removeCache ( pluginContext , filePath ) { const moduleCache = this . requireCache . get ( pluginContext ) ; if ( ! moduleCache ) { return ; } moduleCache . delete ( filePath ) ; } private createModuleSandbox ( pluginContext , filePath ) { const pluginSandbox = this . getPluginSandbox ( pluginContext ) ; const moduleDirname = path . dirname ( filePath ) ; const moduleResolve = Object . assign ( ( id ) => { return this . sandboxResolve ( pluginContext , moduleDirname , id ) ; } , { paths : ( _request ) => null } ) ; const moduleRequire = Object . assign ( ( requiredName ) => { if ( debug . enabled ) { debug ( `Requiring '${ requiredName }' from ${ filePath }...` ) ; } return this . sandboxRequire ( pluginContext , moduleDirname , requiredName ) ; } , { resolve : moduleResolve , cache : { } , extensions : { } as NodeJS . RequireExtensions , main : require . main } ) ; const myModule = { exports : { } , filename : filePath , id : filePath , loaded : false , require : moduleRequire , paths : [ ] , parent : module , children : [ ] , path : moduleDirname } ; const moduleSandbox = { ... pluginSandbox , module : myModule , __dirname : moduleDirname , __filename : filePath , require : moduleRequire } ; return moduleSandbox ; } private sandboxResolve ( pluginContext , moduleDirName , requiredName ) { if ( requiredName . startsWith ( "." ) || path . isAbsolute ( requiredName ) ) { const fullPath = path . resolve ( moduleDirName , requiredName ) ; if ( ! fullPath . startsWith ( pluginContext . location ) ) { throw new Error ( "Cannot require a module outside a plugin" ) ; } const isFile = this . tryResolveAsFile ( fullPath ) ; if ( isFile ) { return isFile ; } const isDirectory = this . tryResolveAsDirectory ( fullPath ) ; if ( isDirectory ) { return isDirectory ; } throw new Error ( `Cannot find ${ requiredName } in plugin ${ pluginContext . name }` ) ; } if ( this . isPlugin ( requiredName ) ) { return requiredName ; } if ( this . manager . options . staticDependencies [ requiredName ] ) { return requiredName ; } if ( this . isCoreModule ( requiredName ) ) { return requiredName ; } return requiredName ; } private sandboxRequire ( pluginContext , moduleDirName , requiredName ) { const fullName = this . sandboxResolve ( pluginContext , moduleDirName , requiredName ) ; if ( path . isAbsolute ( fullName ) ) { if ( debug . enabled ) { debug ( `Resolved ${ requiredName } as file ${ fullName }` ) ; } return this . load ( pluginContext , fullName ) ; } if ( this . manager . options . staticDependencies [ requiredName ] ) { if ( debug . enabled ) { debug ( `Resolved ${ requiredName } as static dependency` ) ; } return this . manager . options . staticDependencies [ requiredName ] ; } if ( this . isPlugin ( requiredName ) ) { if ( debug . enabled ) { debug ( `Resolved ${ requiredName } as plugin` ) ; } return this . manager . require ( requiredName ) ; } if ( this . isCoreModule ( requiredName ) ) { if ( debug . enabled ) { debug ( `Resolved ${ requiredName } as core module` ) ; } return require ( requiredName ) ; } if ( this . manager . options . hostRequire ) { if ( debug . enabled ) { debug ( `Resolved ${ requiredName } as host module` ) ; } return this . manager . options . hostRequire ( requiredName ) ; } throw new Error ( `Module ${ requiredName } not found, failed to load plugin ${ pluginContext . name }` ) ; } private isCoreModule ( requiredName ) { return this . manager . options . requireCoreModules && require . resolve ( requiredName ) === requiredName ; } private isPlugin ( requiredName ) { const { pluginName } = this . splitRequire ( requiredName ) ; return ! ! this . manager . getInfo ( pluginName ) ; } private tryResolveAsFile ( fullPath ) { const parentPath = path . dirname ( fullPath ) ; if ( checkPath ( parentPath ) !== "directory" ) { return undefined ; } const reqPathKind = checkPath ( fullPath ) ; if ( reqPathKind !== "file" ) { if ( checkPath ( fullPath + ".js" ) === "file" ) { return fullPath + ".js" ; } if ( checkPath ( fullPath + ".json" ) === "file" ) { return fullPath + ".json" ; } return undefined ; } if ( reqPathKind === "file" ) { return fullPath ; } return undefined ; } private tryResolveAsDirectory ( fullPath ) { if ( checkPath ( fullPath ) !== "directory" ) { return undefined ; } const indexJs = path . join ( fullPath , "index.js" ) ; if ( checkPath ( indexJs ) === "file" ) { return indexJs ; } const indexJson = path . join ( fullPath , "index.json" ) ; if ( checkPath ( indexJson ) === "file" ) { return indexJson ; } return undefined ; } private getPluginSandbox ( pluginContext ) { let pluginSandbox = this . sandboxCache . get ( pluginContext ) ; if ( ! pluginSandbox ) { const srcSandboxTemplate = this . manager . getSandboxTemplate ( pluginContext . name ) || this . manager . options . sandbox ; pluginSandbox = this . createGlobalSandbox ( srcSandboxTemplate ) ; this . sandboxCache . set ( pluginContext , pluginSandbox ) ; } return pluginSandbox ; } private createGlobalSandbox ( sandboxTemplate ) { const srcGlobal = sandboxTemplate . global || global ; const sandbox = { ... srcGlobal } ; if ( ! sandbox . Buffer && srcGlobal . Buffer ) { sandbox . Buffer = srcGlobal . Buffer ; } if ( ! ( sandbox as any ) . URL && global . URL ) { ( sandbox as any ) . URL = global . URL ; } if ( ! ( sandbox as any ) . URLSearchParams && global . URLSearchParams ) { ( sandbox as any ) . URLSearchParams = global . URLSearchParams ; } if ( ! sandbox . process && global . process ) { sandbox . process = { ... global . process } ; } if ( sandbox . process ) { const srcEnv = sandboxTemplate . env || global . process . env ; sandbox . process . env = { ... srcEnv } ; } if ( ! sandbox . console ) { sandbox . console = new console . Console ( { stdout : process . stdout , stderr : process . stderr } ) ; } sandbox . global = sandbox ; return sandbox ; } } function checkPath ( fullPath ) { try { const stats = fs . statSync ( fullPath ) ; if ( stats . isDirectory ( ) ) { return "directory" ; } else if ( stats . isFile ( ) ) { return "file" ; } else { return "none" ; } } catch { return "none" ; } } interface ModuleSandbox extends NodeJS . Global { module : NodeModule ; __dirname : string ; __filename : string ; require : NodeRequire ; }