2016-09-21 06:27:11 -08:00
|
|
|
const {exec} = require('child_process');
|
|
|
|
|
const {homedir} = require('os');
|
|
|
|
|
const {resolve, basename} = require('path');
|
|
|
|
|
const {writeFileSync} = require('fs');
|
|
|
|
|
|
|
|
|
|
const {app, dialog} = require('electron');
|
|
|
|
|
const {sync: mkdirpSync} = require('mkdirp');
|
2016-07-07 16:16:44 -08:00
|
|
|
const Config = require('electron-config');
|
|
|
|
|
const ms = require('ms');
|
2016-07-17 09:28:24 -08:00
|
|
|
const shellEnv = require('shell-env');
|
2016-07-07 16:16:44 -08:00
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
const config = require('./config');
|
|
|
|
|
const notify = require('./notify');
|
|
|
|
|
|
2016-07-07 16:16:44 -08:00
|
|
|
// local storage
|
|
|
|
|
const cache = new Config();
|
|
|
|
|
|
|
|
|
|
// modules path
|
2016-10-06 07:28:43 -08:00
|
|
|
const path = resolve(homedir(), '.hyper_plugins');
|
|
|
|
|
const localPath = resolve(homedir(), '.hyper_plugins', 'local');
|
2016-07-24 10:03:24 -08:00
|
|
|
const availableExtensions = new Set([
|
|
|
|
|
'onApp', 'onWindow', 'onUnload', 'middleware',
|
2016-10-08 13:38:47 -08:00
|
|
|
'reduceUI', 'reduceSessions', 'reduceTermGroups',
|
|
|
|
|
'decorateMenu', 'decorateTerm', 'decorateHyper',
|
2016-10-08 08:26:07 -08:00
|
|
|
'decorateHyperTerm', // for backwards compatibility with hyperterm
|
2016-10-08 13:38:47 -08:00
|
|
|
'decorateTab',
|
2016-07-24 10:03:24 -08:00
|
|
|
'decorateNotification', 'decorateNotifications',
|
2016-10-08 13:38:47 -08:00
|
|
|
'decorateTabs', 'decorateConfig', 'decorateEnv',
|
|
|
|
|
'decorateTermGroup', 'getTermProps',
|
|
|
|
|
'getTabProps', 'getTabsProps', 'getTermGroupProps',
|
|
|
|
|
'mapHyperTermState', 'mapTermsState',
|
|
|
|
|
'mapHeaderState', 'mapNotificationsState',
|
|
|
|
|
'mapHyperTermDispatch', 'mapTermsDispatch',
|
|
|
|
|
'mapHeaderDispatch', 'mapNotificationsDispatch'
|
2016-07-24 10:03:24 -08:00
|
|
|
]);
|
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();
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function getId(plugins_) {
|
2016-07-07 16:16:44 -08:00
|
|
|
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-09-21 06:27:11 -08:00
|
|
|
function updatePlugins({force = false} = {}) {
|
|
|
|
|
if (updating) {
|
|
|
|
|
return notify('Plugin update in progress');
|
|
|
|
|
}
|
2016-07-07 16:16:44 -08:00
|
|
|
updating = true;
|
|
|
|
|
syncPackageJSON();
|
|
|
|
|
const id_ = id;
|
2016-09-21 06:27:11 -08:00
|
|
|
install(err => {
|
2016-07-07 16:16:44 -08:00
|
|
|
updating = false;
|
|
|
|
|
|
|
|
|
|
if (err) {
|
2016-07-13 18:06:51 -08:00
|
|
|
console.error(err.stack);
|
2016-07-14 08:25:49 -08:00
|
|
|
if (/not a recognized/.test(err.message) || /command not found/.test(err.message)) {
|
|
|
|
|
notify(
|
|
|
|
|
'Error updating plugins.',
|
|
|
|
|
'We could not find the `npm` command. Make sure it\'s in $PATH'
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
notify(
|
|
|
|
|
'Error updating plugins.',
|
2016-10-06 07:28:43 -08:00
|
|
|
'Check `~/.hyper_plugins/npm-debug.log` for more information.'
|
2016-07-14 08:25:49 -08:00
|
|
|
);
|
|
|
|
|
}
|
2016-07-07 16:16:44 -08:00
|
|
|
} else {
|
|
|
|
|
// flag successful plugin update
|
2016-10-06 07:28:43 -08:00
|
|
|
cache.set('hyper.plugins', id_);
|
2016-07-07 16:16:44 -08:00
|
|
|
|
|
|
|
|
// 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-10-06 07:28:43 -08:00
|
|
|
const changed = cache.get('hyper.plugin-versions') !== pluginVersions && loaded === total;
|
|
|
|
|
cache.set('hyper.plugin-versions', pluginVersions);
|
2016-07-13 12:44:24 -08:00
|
|
|
|
|
|
|
|
// notify watchers
|
|
|
|
|
if (force || changed) {
|
2016-07-13 18:06:51 -08:00
|
|
|
if (changed) {
|
|
|
|
|
notify(
|
|
|
|
|
'Plugins Updated',
|
2016-07-16 17:24:16 -08:00
|
|
|
'Restart the app or hot-reload with "View" > "Reload" to enjoy the updates!'
|
2016-07-13 18:06:51 -08:00
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
notify(
|
|
|
|
|
'Plugins Updated',
|
|
|
|
|
'No changes!'
|
|
|
|
|
);
|
|
|
|
|
}
|
2016-09-21 06:27:11 -08:00
|
|
|
watchers.forEach(fn => fn(err, {force}));
|
2016-07-13 12:44:24 -08:00
|
|
|
}
|
2016-07-07 16:16:44 -08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function getPluginVersions() {
|
2016-07-08 06:40:48 -08:00
|
|
|
const paths_ = paths.plugins.concat(paths.localPlugins);
|
2016-09-21 06:27:11 -08:00
|
|
|
return paths_.map(path => {
|
2016-07-08 06:40:48 -08:00
|
|
|
let version = null;
|
|
|
|
|
try {
|
|
|
|
|
version = require(resolve(path, 'package.json')).version;
|
|
|
|
|
} catch (err) { }
|
|
|
|
|
return [
|
|
|
|
|
basename(path),
|
|
|
|
|
version
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function clearCache() {
|
2016-07-13 21:18:06 -08:00
|
|
|
// trigger unload hooks
|
2016-09-21 06:27:11 -08:00
|
|
|
modules.forEach(mod => {
|
|
|
|
|
if (mod.onUnload) {
|
|
|
|
|
mod.onUnload(app);
|
|
|
|
|
}
|
2016-07-13 21:18:06 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// clear require cache
|
2016-07-07 16:16:44 -08:00
|
|
|
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-10-06 07:28:43 -08:00
|
|
|
if (cache.get('hyper.plugins') !== id || process.env.HYPER_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'));
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function syncPackageJSON() {
|
2016-07-07 16:16:44 -08:00
|
|
|
const dependencies = toDependencies(plugins);
|
|
|
|
|
const pkg = {
|
2016-10-06 07:28:43 -08:00
|
|
|
name: 'hyper-plugins',
|
|
|
|
|
description: 'Auto-generated from `~/.hyper.js`!',
|
2016-07-07 16:16:44 -08:00
|
|
|
private: true,
|
|
|
|
|
version: '0.0.1',
|
2016-10-06 07:28:43 -08:00
|
|
|
repository: 'zeit/hyper',
|
2016-07-08 06:40:48 -08:00
|
|
|
license: 'MIT',
|
2016-10-06 07:28:43 -08:00
|
|
|
homepage: 'https://hyper.is',
|
2016-07-07 16:16:44 -08:00
|
|
|
dependencies
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const file = resolve(path, 'package.json');
|
|
|
|
|
try {
|
|
|
|
|
writeFileSync(file, JSON.stringify(pkg, null, 2));
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`An error occurred writing to ${file}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function alert(message) {
|
2016-07-07 16:16:44 -08:00
|
|
|
dialog.showMessageBox({
|
|
|
|
|
message,
|
|
|
|
|
buttons: ['Ok']
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function toDependencies(plugins) {
|
2016-07-07 16:16:44 -08:00
|
|
|
const obj = {};
|
2016-09-21 06:27:11 -08:00
|
|
|
plugins.plugins.forEach(plugin => {
|
2016-08-03 11:39:58 -08:00
|
|
|
const regex = /.(@|#)/;
|
|
|
|
|
const match = regex.exec(plugin);
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
const index = match.index + 1;
|
|
|
|
|
const pieces = [];
|
|
|
|
|
|
|
|
|
|
pieces[0] = plugin.substring(0, index);
|
|
|
|
|
pieces[1] = plugin.substring(index + 1, plugin.length);
|
|
|
|
|
obj[pieces[0]] = pieces[1];
|
|
|
|
|
} else {
|
|
|
|
|
obj[plugin] = 'latest';
|
|
|
|
|
}
|
2016-07-07 16:16:44 -08:00
|
|
|
});
|
|
|
|
|
return obj;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function install(fn) {
|
|
|
|
|
const {shell: cfgShell, npmRegistry} = exports.getDecoratedConfig();
|
2016-07-30 10:51:09 -08:00
|
|
|
|
|
|
|
|
const shell = cfgShell && cfgShell !== '' ? cfgShell : undefined;
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
shellEnv(shell).then(env => {
|
|
|
|
|
if (npmRegistry) {
|
|
|
|
|
env.NPM_CONFIG_REGISTRY = npmRegistry;
|
|
|
|
|
}
|
|
|
|
|
/* eslint-disable camelcase */
|
2016-07-19 15:22:56 -08:00
|
|
|
env.npm_config_runtime = 'electron';
|
2016-07-26 15:39:11 -08:00
|
|
|
env.npm_config_target = '1.3.0';
|
2016-07-19 15:22:56 -08:00
|
|
|
env.npm_config_disturl = 'https://atom.io/download/atom-shell';
|
2016-09-21 06:27:11 -08:00
|
|
|
/* eslint-enable camelcase */
|
2016-07-30 10:51:09 -08:00
|
|
|
exec('npm prune; npm install --production', {
|
2016-07-17 09:28:24 -08:00
|
|
|
cwd: path,
|
2016-07-30 10:51:09 -08:00
|
|
|
env,
|
|
|
|
|
shell
|
2016-09-21 06:27:11 -08:00
|
|
|
}, err => {
|
|
|
|
|
if (err) {
|
|
|
|
|
return fn(err);
|
|
|
|
|
}
|
2016-07-17 09:28:24 -08:00
|
|
|
fn(null);
|
|
|
|
|
});
|
|
|
|
|
}).catch(fn);
|
2016-07-07 16:16:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exports.subscribe = function (fn) {
|
|
|
|
|
watchers.push(fn);
|
|
|
|
|
return () => {
|
|
|
|
|
watchers.splice(watchers.indexOf(fn), 1);
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function getPaths() {
|
2016-07-07 16:16:44 -08:00
|
|
|
return {
|
2016-09-21 06:27:11 -08:00
|
|
|
plugins: plugins.plugins.map(name => {
|
2016-07-13 18:06:51 -08:00
|
|
|
return resolve(path, 'node_modules', name.split('#')[0]);
|
2016-07-07 16:16:44 -08:00
|
|
|
}),
|
2016-09-21 06:27:11 -08:00
|
|
|
localPlugins: plugins.localPlugins.map(name => {
|
2016-07-07 16:16:44 -08:00
|
|
|
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 () {
|
2016-09-21 06:27:11 -08:00
|
|
|
return {path, localPath};
|
2016-07-08 08:48:37 -08:00
|
|
|
};
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
function requirePlugins() {
|
|
|
|
|
const {plugins, localPlugins} = paths;
|
2016-07-07 16:16:44 -08:00
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
const load = path => {
|
2016-07-07 16:16:44 -08:00
|
|
|
let mod;
|
|
|
|
|
try {
|
|
|
|
|
mod = require(path);
|
2016-07-24 10:03:24 -08:00
|
|
|
const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key));
|
|
|
|
|
if (!exposed) {
|
2016-07-07 16:16:44 -08:00
|
|
|
notify('Plugin error!', `Plugin "${basename(path)}" does not expose any ` +
|
2016-10-06 07:28:43 -08:00
|
|
|
'Hyper extension API methods');
|
2016-07-07 16:16:44 -08:00
|
|
|
return;
|
|
|
|
|
}
|
2016-07-16 10:59:20 -08:00
|
|
|
|
|
|
|
|
// populate the name for internal errors here
|
|
|
|
|
mod._name = basename(path);
|
|
|
|
|
|
2016-07-07 16:16:44 -08:00
|
|
|
return mod;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
notify('Plugin error!', `Plugin "${basename(path)}" failed to load (${err.message})`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return plugins.map(load)
|
|
|
|
|
.concat(localPlugins.map(load))
|
2016-09-21 06:27:11 -08:00
|
|
|
.filter(v => Boolean(v));
|
2016-07-07 16:16:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exports.onApp = function (app) {
|
2016-09-21 06:27:11 -08:00
|
|
|
modules.forEach(plugin => {
|
2016-07-07 16:16:44 -08:00
|
|
|
if (plugin.onApp) {
|
|
|
|
|
plugin.onApp(app);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2016-07-16 14:37:27 -08:00
|
|
|
exports.onWindow = function (win) {
|
2016-09-21 06:27:11 -08:00
|
|
|
modules.forEach(plugin => {
|
2016-07-07 16:16:44 -08:00
|
|
|
if (plugin.onWindow) {
|
2016-07-16 14:37:27 -08:00
|
|
|
plugin.onWindow(win);
|
2016-07-07 16:16:44 -08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2016-07-24 10:03:24 -08:00
|
|
|
// decorates the base object by calling plugin[key]
|
|
|
|
|
// for all the available plugins
|
2016-09-21 06:27:11 -08:00
|
|
|
function decorateObject(base, key) {
|
2016-07-24 10:03:24 -08:00
|
|
|
let decorated = base;
|
2016-09-21 06:27:11 -08:00
|
|
|
modules.forEach(plugin => {
|
2016-07-24 10:03:24 -08:00
|
|
|
if (plugin[key]) {
|
|
|
|
|
const res = plugin[key](decorated);
|
2016-09-21 06:27:11 -08:00
|
|
|
if (res && typeof res === 'object') {
|
2016-07-08 13:27:02 -08:00
|
|
|
decorated = res;
|
|
|
|
|
} else {
|
2016-07-24 10:03:24 -08:00
|
|
|
notify('Plugin error!', `"${plugin._name}": invalid return type for \`${key}\``);
|
2016-07-08 13:27:02 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2016-07-24 10:03:24 -08:00
|
|
|
|
2016-07-08 13:27:02 -08:00
|
|
|
return decorated;
|
2016-07-24 10:03:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exports.decorateMenu = function (tpl) {
|
|
|
|
|
return decorateObject(tpl, 'decorateMenu');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
exports.getDecoratedEnv = function (baseEnv) {
|
|
|
|
|
return decorateObject(baseEnv, 'decorateEnv');
|
2016-07-07 16:16:44 -08:00
|
|
|
};
|
|
|
|
|
|
2016-07-16 10:59:20 -08:00
|
|
|
exports.getDecoratedConfig = function () {
|
2016-07-24 10:03:24 -08:00
|
|
|
const baseConfig = config.getConfig();
|
|
|
|
|
return decorateObject(baseConfig, 'decorateConfig');
|
2016-07-07 16:16:44 -08:00
|
|
|
};
|
2016-07-21 16:32:39 -08:00
|
|
|
|
|
|
|
|
exports.getDecoratedBrowserOptions = function (defaults) {
|
2016-07-24 10:03:24 -08:00
|
|
|
return decorateObject(defaults, 'decorateBrowserOptions');
|
2016-07-21 16:32:39 -08:00
|
|
|
};
|
2016-08-03 11:39:58 -08:00
|
|
|
|
|
|
|
|
exports._toDependencies = toDependencies;
|