live-plugin-manager
Version:
Install and uninstall any node package at runtime from npm registry
421 lines • 17 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginVm = void 0;
const vm = __importStar(require("vm"));
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const console = __importStar(require("console"));
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)("live-plugin-manager.PluginVm");
const SCOPED_REGEX = /^(@[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)(.*)/;
class PluginVm {
constructor(manager) {
this.manager = manager;
this.requireCache = new Map();
this.sandboxCache = new Map();
}
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" || filePathExtension === ".cjs") {
const code = fs.readFileSync(filePath, "utf8");
// note: I first put the object (before executing the script) in cache to support circular require
this.setCache(pluginContext, filePath, moduleInstance);
try {
this.vmRunScriptInSandbox(sandbox, filePath, code);
}
catch (e) {
// in case of error remove the cache
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: {},
dependencyDetails: {}
};
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 };
}
getScopedInfo(fullName) {
const match = SCOPED_REGEX.exec(fullName);
if (!match) {
return undefined;
}
const requiredPath = match[2]
? "." + match[2]
: undefined;
return {
pluginName: match[1],
requiredPath
};
}
vmRunScriptInSandbox(moduleSandbox, filePath, code) {
const moduleContext = vm.createContext(moduleSandbox);
// For performance reasons wrap code in a Immediately-invoked function expression
// https://60devs.com/executing-js-code-with-nodes-vm-module.html
// I have also declared the exports variable to support the
// `var app = exports = module.exports = {};` notation
const iifeCode = `
(function(exports){
${code}
}(module.exports));`;
const vmOptions = { displayErrors: true, filename: filePath };
const script = new vm.Script(iifeCode, vmOptions);
script.runInContext(moduleContext, vmOptions);
}
vmRunScriptInPlugin(pluginContext, filePath, code) {
const sandbox = this.createModuleSandbox(pluginContext, filePath);
this.vmRunScriptInSandbox(sandbox, filePath, code);
sandbox.module.loaded = true;
return sandbox.module.exports;
}
getCache(pluginContext, filePath) {
const moduleCache = this.requireCache.get(pluginContext);
if (!moduleCache) {
return undefined;
}
return moduleCache.get(filePath);
}
setCache(pluginContext, filePath, instance) {
let moduleCache = this.requireCache.get(pluginContext);
if (!moduleCache) {
moduleCache = new Map();
this.requireCache.set(pluginContext, moduleCache);
}
moduleCache.set(filePath, instance);
}
removeCache(pluginContext, filePath) {
const moduleCache = this.requireCache.get(pluginContext);
if (!moduleCache) {
return;
}
moduleCache.delete(filePath);
}
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 // TODO I should I populate this
});
const moduleRequire = Object.assign((requiredName) => {
if (debug.enabled) {
debug(`Requiring '${requiredName}' from ${filePath}...`);
}
return this.sandboxRequire(pluginContext, moduleDirname, requiredName);
}, {
resolve: moduleResolve,
cache: {},
extensions: {},
main: require.main // TODO assign the real main or consider main the current module (ie. module)?
});
const myModule = {
exports: {},
filename: filePath,
id: filePath,
loaded: false,
require: moduleRequire,
paths: [],
parent: module,
children: [],
path: moduleDirname,
isPreloading: false
};
// assign missing https://nodejs.org/api/globals.html
// and other "not real global" objects
const moduleSandbox = Object.assign(Object.assign({}, pluginSandbox), { module: myModule, __dirname: moduleDirname, __filename: filePath, require: moduleRequire });
return moduleSandbox;
}
sandboxResolve(pluginContext, moduleDirName, requiredName) {
// I try to use a similar logic of https://nodejs.org/api/modules.html#modules_modules
// is a relative module or absolute path
if (requiredName.startsWith(".") || path.isAbsolute(requiredName)) {
const fullPath = path.resolve(moduleDirName, requiredName);
// for security reason check to not load external files
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.hasDependency(pluginContext, requiredName)) {
let fullPath = path.join(pluginContext.location, "node_modules", requiredName);
if (!pluginContext.dependencyDetails) {
throw new Error(`Dependencies not loaded for plugin ${pluginContext.name}`);
}
const packageJson = pluginContext.dependencyDetails[requiredName];
if (!packageJson) {
throw new Error(`${pluginContext.name} does not include ${requiredName} in local dependencies`);
}
if (packageJson.main) {
fullPath = path.join(fullPath, packageJson.main);
}
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;
}
// this will fail if module is unknown
if (this.isCoreModule(requiredName)) {
return requiredName;
}
return requiredName;
}
sandboxRequire(pluginContext, moduleDirName, requiredName) {
// I try to use a similar logic of https://nodejs.org/api/modules.html#modules_modules
const fullName = this.sandboxResolve(pluginContext, moduleDirName, requiredName);
// is an absolute file or directory that can be loaded
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); // I use system require
}
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}`);
}
isCoreModule(requiredName) {
return this.manager.options.requireCoreModules
&& require.resolve(requiredName) === requiredName;
}
isPlugin(requiredName) {
const { pluginName } = this.splitRequire(requiredName);
return !!this.manager.getInfo(pluginName);
}
hasDependency(pluginContext, requiredName) {
const { dependencyDetails } = pluginContext;
if (!dependencyDetails) {
return false;
}
return !!dependencyDetails[requiredName];
}
tryResolveAsFile(fullPath) {
const parentPath = path.dirname(fullPath);
if (checkPath(parentPath) !== "directory") {
return undefined;
}
const reqPathKind = checkPath(fullPath);
if (reqPathKind !== "file") {
if (checkPath(fullPath + ".cjs") === "file") {
return fullPath + ".cjs";
}
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;
}
tryResolveAsDirectory(fullPath) {
if (checkPath(fullPath) !== "directory") {
return undefined;
}
const indexCjs = path.join(fullPath, "index.cjs");
if (checkPath(indexCjs) === "file") {
return indexCjs;
}
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;
}
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;
}
createGlobalSandbox(sandboxTemplate) {
const srcGlobal = sandboxTemplate.global || global;
const sandbox = Object.assign({}, srcGlobal);
// copy properties that are not copied automatically (don't know why..)
// https://stackoverflow.com/questions/59009214/some-properties-of-the-global-instance-are-not-copied-by-spread-operator-or-by-o
// (some of these properties are Node.js specific, like Buffer)
// Function and Object should not be defined, otherwise we will have some unexpected behavior
// Somewhat related to https://github.com/nodejs/node/issues/28823
if (!sandbox.Buffer && srcGlobal.Buffer) {
sandbox.Buffer = srcGlobal.Buffer;
}
if (!sandbox.URL && global.URL) {
// cast to any because URL is not defined inside NodeJSGlobal, I don't understand why ...
sandbox.URL = global.URL;
}
if (!sandbox.URLSearchParams && global.URLSearchParams) {
// cast to any because URLSearchParams is not defined inside NodeJSGlobal, I don't understand why ...
sandbox.URLSearchParams = global.URLSearchParams;
}
if (!sandbox.process && global.process) {
sandbox.process = Object.assign({}, global.process);
}
if (sandbox.process) {
// override env to "unlink" from original process
const srcEnv = sandboxTemplate.env || global.process.env;
sandbox.process.env = Object.assign({}, srcEnv); // copy properties
sandbox.process.on = (event, callback) => { };
}
// create global console
if (!sandbox.console) {
sandbox.console = new console.Console({ stdout: process.stdout, stderr: process.stderr });
}
// override the global obj to "unlink" it from the original global obj
// and make it unique for each sandbox
sandbox.global = sandbox;
return sandbox;
}
}
exports.PluginVm = PluginVm;
function checkPath(fullPath) {
try {
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
return "directory";
}
else if (stats.isFile()) {
return "file";
}
else {
return "none";
}
}
catch (_a) {
return "none";
}
}
//# sourceMappingURL=PluginVm.js.map