hyper/plugins.js

290 lines
7 KiB
JavaScript
Raw Normal View History

2016-07-07 16:16:44 -08:00
const { dialog } = require('electron');
const { homedir } = require('os');
const { resolve, basename } = require('path');
const { writeFileSync } = require('fs');
const config = require('./config');
const { sync: mkdirpSync } = require('mkdirp');
const { exec } = require('child_process');
const Config = require('electron-config');
const ms = require('ms');
const notify = require('./notify');
// local storage
const cache = new Config();
// modules path
const path = resolve(homedir(), '.hyperterm_modules');
const localPath = resolve(homedir(), '.hyperterm_modules', 'local');
2016-07-07 16:16:44 -08:00
// init plugin directories if not present
mkdirpSync(path);
mkdirpSync(localPath);
// caches
let plugins = config.getPlugins();
let paths = getPaths(plugins);
let id = getId(plugins);
let modules = requirePlugins();
function getId (plugins_) {
return JSON.stringify(plugins_);
}
const watchers = [];
// we listen on configuration updates to trigger
// plugin installation
config.subscribe(() => {
const plugins_ = config.getPlugins();
if (plugins !== plugins_) {
const id_ = getId(plugins_);
if (id !== id_) {
id = id_;
plugins = plugins_;
updatePlugins();
}
}
});
let updating = false;
2016-07-08 06:40:48 -08:00
function updatePlugins ({ force = false } = {}) {
2016-07-07 16:16:44 -08:00
if (updating) return notify('Plugin update in progress');
updating = true;
syncPackageJSON();
const id_ = id;
install((err) => {
updating = false;
if (err) {
console.error(err.stack);
2016-07-07 16:16:44 -08:00
notify(
'Error updating plugins.',
'Check `~/.hyperterm_modules/npm-debug.log` for more information.'
);
} else {
// flag successful plugin update
cache.set('plugins', id_);
// cache paths
paths = getPaths(plugins);
// clear require cache
2016-07-08 08:46:37 -08:00
clearCache();
2016-07-07 16:16:44 -08:00
// cache modules
modules = requirePlugins();
2016-07-08 06:40:48 -08:00
const loaded = modules.length;
const total = paths.plugins.length + paths.localPlugins.length;
const pluginVersions = JSON.stringify(getPluginVersions());
2016-07-13 12:44:24 -08:00
const changed = cache.get('plugin-versions') !== pluginVersions && loaded === total;
2016-07-08 06:40:48 -08:00
cache.set('plugin-versions', pluginVersions);
2016-07-13 12:44:24 -08:00
// notify watchers
if (force || changed) {
if (changed) {
notify(
'Plugins Updated',
'Restart the app or hot-reload with "Plugins" > "Reload Now" to enjoy the updates!'
);
} else {
notify(
'Plugins Updated',
'No changes!'
);
}
2016-07-13 12:44:24 -08:00
watchers.forEach((fn) => fn(err, { force }));
}
2016-07-07 16:16:44 -08:00
}
});
}
2016-07-08 06:40:48 -08:00
function getPluginVersions () {
const paths_ = paths.plugins.concat(paths.localPlugins);
return paths_.map((path) => {
let version = null;
try {
version = require(resolve(path, 'package.json')).version;
} catch (err) { }
return [
basename(path),
version
];
});
}
2016-07-07 16:16:44 -08:00
function clearCache (mod) {
for (const entry in require.cache) {
2016-07-08 08:46:37 -08:00
if (entry.indexOf(path) === 0 || entry.indexOf(localPath) === 0) {
2016-07-07 16:16:44 -08:00
delete require.cache[entry];
}
}
}
exports.updatePlugins = updatePlugins;
// we schedule the initial plugins update
// a bit after the user launches the terminal
// to prevent slowness
2016-07-08 06:40:48 -08:00
if (cache.get('plugins') !== id || process.env.HYPERTERM_FORCE_UPDATE) {
2016-07-07 16:16:44 -08:00
// install immediately if the user changed plugins
console.log('plugins have changed / not init, scheduling plugins installation');
setTimeout(() => {
updatePlugins();
}, 5000);
}
// otherwise update plugins every 5 hours
setInterval(updatePlugins, ms('5h'));
function syncPackageJSON () {
const dependencies = toDependencies(plugins);
const pkg = {
name: 'hyperterm-plugins',
2016-07-08 06:40:48 -08:00
description: 'Auto-generated from `~/.hyperterm.js`!',
2016-07-07 16:16:44 -08:00
private: true,
version: '0.0.1',
2016-07-08 06:40:48 -08:00
repository: 'zeit/hyperterm',
license: 'MIT',
2016-07-07 16:16:44 -08:00
homepage: 'https://hyperterm.org',
dependencies
};
const file = resolve(path, 'package.json');
try {
writeFileSync(file, JSON.stringify(pkg, null, 2));
} catch (err) {
alert(`An error occurred writing to ${file}`);
}
}
function alert (message) {
dialog.showMessageBox({
message,
buttons: ['Ok']
});
}
function toDependencies (plugins) {
const obj = {};
plugins.plugins.forEach((plugin) => {
const pieces = plugin.split('#');
obj[pieces[0]] = null == pieces[1] ? 'latest' : pieces[1];
});
return obj;
}
function install (fn) {
const prefix = 'darwin' === process.platform ? 'eval `/usr/libexec/path_helper -s` && ' : '';
exec(prefix + 'npm prune && npm install --production', {
cwd: path
}, (err, stdout, stderr) => {
if (err) alert(err.stack);
if (err) return fn(err);
fn(null);
2016-07-07 16:16:44 -08:00
});
}
exports.subscribe = function (fn) {
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
};
};
function getPaths () {
return {
plugins: plugins.plugins.map((name) => {
return resolve(path, 'node_modules', name.split('#')[0]);
2016-07-07 16:16:44 -08:00
}),
localPlugins: plugins.localPlugins.map((name) => {
return resolve(localPath, name);
})
};
}
2016-07-08 08:48:37 -08:00
// expose to renderer
2016-07-07 16:16:44 -08:00
exports.getPaths = getPaths;
2016-07-08 08:48:37 -08:00
// get paths from renderer
exports.getBasePaths = function () {
return { path, localPath };
};
2016-07-07 16:16:44 -08:00
function requirePlugins () {
const { plugins, localPlugins } = paths;
const load = (path) => {
let mod;
try {
mod = require(path);
if (!mod || (!mod.onApp && !mod.onWindow && !mod.onUnload &&
2016-07-13 12:44:24 -08:00
!mod.middleware &&
2016-07-07 16:16:44 -08:00
!mod.decorateConfig && !mod.decorateMenu &&
!mod.decorateTerm && !mod.decorateHyperTerm &&
2016-07-13 12:44:24 -08:00
!mod.decorateTab && !mod.decorateNotification &&
!mod.decorateNotifications && !mod.decorateTabs &&
!mod.decorateConfig)) {
2016-07-07 16:16:44 -08:00
notify('Plugin error!', `Plugin "${basename(path)}" does not expose any ` +
'HyperTerm extension API methods');
return;
}
return mod;
} catch (err) {
notify('Plugin error!', `Plugin "${basename(path)}" failed to load (${err.message})`);
}
};
return plugins.map(load)
.concat(localPlugins.map(load))
.filter(v => !!v);
}
exports.onApp = function (app) {
modules.forEach((plugin) => {
if (plugin.onApp) {
plugin.onApp(app);
}
});
};
exports.onWindow = function (win, app) {
modules.forEach((plugin) => {
if (plugin.onWindow) {
plugin.onWindow(app);
}
});
};
exports.decorateMenu = function (tpl) {
2016-07-08 13:27:02 -08:00
let decorated = tpl;
modules.forEach((plugin) => {
if (plugin.decorateMenu) {
const res = plugin.decorateMenu(decorated);
if (res) {
decorated = res;
} else {
console.error('incompatible response type for `decorateMenu`');
}
}
});
return decorated;
2016-07-07 16:16:44 -08:00
};
exports.decorateConfig = function (config) {
let decorated = config;
modules.forEach((plugin) => {
if (plugin.decorateConfig) {
const res = plugin.decorateConfig(decorated);
if (res) {
decorated = res;
} else {
console.error('incompatible response type for `decorateConfig`');
}
}
});
return decorated;
};