@egeria/plugin-manager
Version:
Plugin loader and updater
194 lines (171 loc) • 7.29 kB
JavaScript
/*
.--. .-'. .--. .--. .--. .--. .`-. .--.
:::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\
' `--' `.-' `--' `--' `--' `-.' `--' `
Egeria - She bestows Knowledge and Wisdom
Copyright (C) 2016-2019 MySidesTheyAreGone <mysidestheyaregone@protonmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
.--. .-'. .--. .--. .--. .--. .`-. .--.
:::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\
' `--' `.-' `--' `--' `--' `-.' `--' `
*/
module.exports = function (state) {
const R = require('ramda')
const T = require('@egeria/tools')
const W = require('@egeria/httplib')
const FS = require('@egeria/fslib')
const semver = require('semver')
const path = require('path')
const _spawn = require('child_process').spawn
const prefix = 'PLUGIN MANAGER |'
state.set('prefix', prefix)
const err = (message, origin, data) => T.err(prefix, message, origin, data)
const { logSilly, logInfo, logWarning, logError } = T.setupLogging()
const rethrow = (action, e, data) => {
throw T.err(prefix, action, e, data)
}
const moduleBaseDir = state.get('moduleBaseDir')
const moduleInstallDir = 'lib/node_modules'
const pluginRoster = state.select('plugins')
const installCmd = 'install --silent --prefix=' + moduleBaseDir + ' --global --production '
let loadedPlugins = {}
function cleanOutput (d) {
return R.replace(/\r?\n|\r/, '', d.toString())
}
const versionSatisfies = R.flip(R.binary(semver.satisfies))
async function spawn (cmd, args) {
return new Promise((resolve, reject) => {
var proc = _spawn(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'] })
proc.stderr.on('data', (d) => {
logWarning(state, 'npm warns: ' + cleanOutput(d))
})
proc.on('error', (code) => {
var e = new Error('npm terminated abnormally with code "' + code + '"')
reject(e)
})
proc.on('close', (code, signal) => {
if (code !== 0 || signal === 'SIGTERM') {
var reason = (R.isNil(signal) ? ' - exit code ' + code : ' - received signal ' + signal)
var e = new Error('npm terminated abnormally' + reason)
reject(e)
} else {
resolve(true)
}
})
})
}
function getDirectory (module) {
return path.resolve(moduleBaseDir, moduleInstallDir, module)
}
async function getLatestVersion (module) {
let { body } = await W.httpreq({ uri: 'https://registry.npmjs.org/' + encodeURIComponent(module).replace('%40', '@'), json: true })
let versions = R.keys(body.versions)
let allowedVersions = R.filter(versionSatisfies('^0.x'), versions)
let latest = R.last(allowedVersions)
logSilly(state, 'Latest allowed ' + module + ' version is v' + latest)
return latest
}
async function getInstalledVersion (module) {
let dir = getDirectory(module)
let version
try {
let pkgfile = await FS.read(path.resolve(dir, 'package.json'))
version = JSON.parse(pkgfile).version
} catch (e) {
version = null
}
logSilly(state, 'Installed ' + module + ' version is v' + version)
return version
}
function remove (module) {
return FS.erase(getDirectory(module))
}
async function install (module) {
var action = 'While installing module ' + module
try {
await spawn('npm', R.split(' ', installCmd + module))
} catch (e) {
rethrow(action, e)
}
}
async function load (name) {
logSilly(state, 'Attempting to load ' + name)
let action = 'While attempting to load plugin ' + name
let module = pluginRoster.get(name)
if (R.isNil(module)) {
logInfo(state, `Plugin "${name}" is not official`)
if (!R.isNil(loadedPlugins[name])) {
return loadedPlugins[name][name]
}
try {
loadedPlugins[name] = require(path.resolve('..', `plugin-${name}`))
return loadedPlugins[name][name]
} catch (e) {
logSilly(state, e.message)
throw err(action, new Error(`Plugin ${name} doesn't exist and can't be found.`))
}
} else if (!R.isNil(loadedPlugins[module])) {
return loadedPlugins[module][name]
} else {
try {
let repoPluginDir = path.resolve('..', 'plugin-' + R.replace(/@egeria\/(.*)-plugin/, '$1', module))
loadedPlugins[module] = require(repoPluginDir)
return loadedPlugins[module][name]
} catch (e) {
logSilly(state, e.message)
}
let moduleDir = getDirectory(module)
let moduleIsOutdated
let registryUnreachable = false
let current = await getInstalledVersion(module)
let latest
logSilly(state, 'Getting latest version of ' + module)
try {
latest = await getLatestVersion(module)
moduleIsOutdated = R.isNil(current) || semver.gt(latest, current)
} catch (e) {
logError(state, `Couldn't get the latest version number of ${module} from the npm registry`, e)
latest = null
moduleIsOutdated = false
registryUnreachable = true
}
let moduleDirExists = await FS.exists(moduleDir)
try {
if (moduleIsOutdated) {
if (moduleDirExists) {
logInfo(state, 'Upgrading ' + module + ' to v' + latest)
await remove(module)
} else {
logInfo(state, 'Installing ' + module + '@v' + latest)
}
await install(module)
}
logSilly(state, 'Requiring ' + module)
let plugin = require(moduleDir)
logInfo(state, 'Plugin ' + name + ' loaded')
loadedPlugins[module] = plugin
return plugin[name]
} catch (e) {
if (moduleIsOutdated) {
throw err(`The installation of ${module} failed or resulted in a corrupted module: giving up`, e)
} else if (registryUnreachable) {
throw err(`The module ${module} is either missing or corrupted; since it was also impossibile to fetch anything from the npm registry, I'm giving up. Check your Internet connection.`, e)
} else {
logError(state, 'You have the latest version of ' + module + ', but it seems to be corrupted. Attempting reinstallation.')
await remove(module)
return load(name)
}
}
}
}
return { load }
}