hyper/app/plugins.js

413 lines
11 KiB
JavaScript
Raw Normal View History

2017-08-13 23:42:28 -08:00
const {app, dialog} = require('electron');
const {resolve, basename} = require('path');
const {writeFileSync} = require('fs');
2016-07-07 16:16:44 -08:00
const Config = require('electron-config');
const ms = require('ms');
const React = require('react');
const ReactDom = require('react-dom');
const config = require('./config');
const notify = require('./notify');
const {availableExtensions} = require('./plugins/extensions');
const {install} = require('./plugins/install');
const {plugs} = require('./config/paths');
2018-04-16 06:17:17 -08:00
const mapKeys = require('./utils/map-keys');
2016-07-07 16:16:44 -08:00
// local storage
const cache = new Config();
const path = plugs.base;
const localPath = plugs.local;
2016-07-07 16:16:44 -08:00
patchModuleLoad();
2016-07-07 16:16:44 -08:00
// caches
let plugins = config.getPlugins();
2018-04-16 06:17:17 -08:00
let paths = getPaths();
2016-07-07 16:16:44 -08:00
let id = getId(plugins);
let modules = requirePlugins();
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();
}
}
});
// patching Module._load
// so plugins can `require` them without needing their own version
// https://github.com/zeit/hyper/issues/619
function patchModuleLoad() {
const Module = require('module');
const originalLoad = Module._load;
Module._load = function _load(modulePath) {
// PLEASE NOTE: Code changes here, also need to be changed in
// lib/utils/plugins.js
switch (modulePath) {
case 'react':
// DEPRECATED
return React;
case 'react-dom':
// DEPRECATED
return ReactDom;
case 'hyper/component':
// DEPRECATED
return React.PureComponent;
// These return Object, since they work differently on the backend, than on the frontend.
// Still needs to be here, to prevent errors, while loading plugins.
case 'hyper/Notification':
case 'hyper/notify':
case 'hyper/decorate':
return Object;
default:
return originalLoad.apply(this, arguments);
}
};
}
2018-04-16 06:17:17 -08:00
function checkDeprecatedExtendKeymaps() {
modules.forEach(plugin => {
if (plugin.extendKeymaps) {
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
return;
}
});
}
2016-07-07 16:16:44 -08:00
let updating = false;
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;
install(err => {
2016-07-07 16:16:44 -08:00
updating = false;
if (err) {
2018-04-16 06:17:17 -08:00
//eslint-disable-next-line no-console
notify('Error updating plugins.', err, {error: err});
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
2018-04-16 06:17:17 -08:00
paths = getPaths();
2016-07-07 16:16:44 -08:00
// 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
2018-04-16 06:17:17 -08:00
watchers.forEach(fn => fn(err, {force}));
2016-07-13 12:44:24 -08:00
if (force || changed) {
if (changed) {
2018-04-16 06:17:17 -08:00
notify('Plugins Updated', 'Restart the app or hot-reload with "View" > "Reload" to enjoy the updates!');
} else {
2018-04-16 06:17:17 -08:00
notify('Plugins Updated', 'No changes!');
}
2018-04-16 06:17:17 -08:00
checkDeprecatedExtendKeymaps();
2016-07-13 12:44:24 -08:00
}
2016-07-07 16:16:44 -08:00
}
});
}
function getPluginVersions() {
2016-07-08 06:40:48 -08:00
const paths_ = paths.plugins.concat(paths.localPlugins);
2018-04-16 06:17:17 -08:00
return paths_.map(path_ => {
2016-07-08 06:40:48 -08:00
let version = null;
try {
2018-04-16 06:17:17 -08:00
//eslint-disable-next-line import/no-dynamic-require
version = require(resolve(path_, 'package.json')).version;
//eslint-disable-next-line no-empty
} catch (err) {}
return [basename(path_), version];
2016-07-08 06:40:48 -08:00
});
}
function clearCache() {
2016-07-13 21:18:06 -08:00
// trigger unload hooks
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;
2018-04-16 06:17:17 -08:00
exports.getLoadedPluginVersions = () => {
return modules.map(mod => ({name: mod._name, version: mod._version}));
};
2016-07-07 16:16:44 -08:00
// 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
2018-04-16 06:17:17 -08:00
//eslint-disable-next-line no-console
2016-07-07 16:16:44 -08:00
console.log('plugins have changed / not init, scheduling plugins installation');
setTimeout(() => {
updatePlugins();
2018-04-16 06:17:17 -08:00
}, 1000);
2016-07-07 16:16:44 -08:00
}
// otherwise update plugins every 5 hours
setInterval(updatePlugins, ms('5h'));
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}`);
}
}
function alert(message) {
2016-07-07 16:16:44 -08:00
dialog.showMessageBox({
message,
buttons: ['Ok']
});
}
2018-04-16 06:17:17 -08:00
function toDependencies(plugins_) {
2016-07-07 16:16:44 -08:00
const obj = {};
2018-04-16 06:17:17 -08:00
plugins_.plugins.forEach(plugin => {
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;
}
2018-04-16 06:17:17 -08:00
exports.subscribe = fn => {
2016-07-07 16:16:44 -08:00
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
};
};
function getPaths() {
2016-07-07 16:16:44 -08:00
return {
plugins: plugins.plugins.map(name => {
2018-04-16 06:17:17 -08:00
return resolve(path, 'node_modules', name.split('#')[0].split('@')[0]);
2016-07-07 16:16:44 -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
2018-04-16 06:17:17 -08:00
exports.getBasePaths = () => {
return {path, localPath};
2016-07-08 08:48:37 -08:00
};
function requirePlugins() {
2018-04-16 06:17:17 -08:00
const {plugins: plugins_, localPlugins} = paths;
2016-07-07 16:16:44 -08:00
2018-04-16 06:17:17 -08:00
const load = path_ => {
2016-07-07 16:16:44 -08:00
let mod;
try {
// eslint-disable-next-line import/no-dynamic-require
2018-04-16 06:17:17 -08:00
mod = require(path_);
const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key));
if (!exposed) {
2018-04-16 06:17:17 -08:00
notify('Plugin error!', `Plugin "${basename(path_)}" does not expose any ` + 'Hyper extension API methods');
2016-07-07 16:16:44 -08:00
return;
}
// populate the name for internal errors here
2018-04-16 06:17:17 -08:00
mod._name = basename(path_);
try {
// eslint-disable-next-line import/no-dynamic-require
2018-04-16 06:17:17 -08:00
mod._version = require(resolve(path_, 'package.json')).version;
} catch (err) {
2018-04-16 06:17:17 -08:00
//eslint-disable-next-line no-console
console.warn(`No package.json found in ${path_}`);
}
2018-04-16 06:17:17 -08:00
//eslint-disable-next-line no-console
console.log(`Plugin ${mod._name} (${mod._version}) loaded.`);
2016-07-07 16:16:44 -08:00
return mod;
} catch (err) {
2018-04-16 06:17:17 -08:00
if (err.code === 'MODULE_NOT_FOUND') {
//eslint-disable-next-line no-console
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
2018-04-16 06:17:17 -08:00
} else {
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err});
2018-04-16 06:17:17 -08:00
}
2016-07-07 16:16:44 -08:00
}
};
2018-04-16 06:17:17 -08:00
return plugins_
.map(load)
2016-07-07 16:16:44 -08:00
.concat(localPlugins.map(load))
.filter(v => Boolean(v));
2016-07-07 16:16:44 -08:00
}
2018-04-16 06:17:17 -08:00
exports.onApp = app_ => {
modules.forEach(plugin => {
2016-07-07 16:16:44 -08:00
if (plugin.onApp) {
2018-04-16 06:17:17 -08:00
try {
plugin.onApp(app_);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
error: e
});
2018-04-16 06:17:17 -08:00
}
2016-07-07 16:16:44 -08:00
}
});
};
2018-04-16 06:17:17 -08:00
exports.onWindow = win => {
modules.forEach(plugin => {
2016-07-07 16:16:44 -08:00
if (plugin.onWindow) {
2018-04-16 06:17:17 -08:00
try {
plugin.onWindow(win);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
error: e
});
2018-04-16 06:17:17 -08:00
}
2016-07-07 16:16:44 -08:00
}
});
};
// decorates the base object by calling plugin[key]
// for all the available plugins
function decorateObject(base, key) {
let decorated = base;
modules.forEach(plugin => {
if (plugin[key]) {
2018-04-16 06:17:17 -08:00
let res;
try {
res = plugin[key](decorated);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" when decorating ${key}`, {error: e});
2018-04-16 06:17:17 -08:00
return;
}
if (res && typeof res === 'object') {
2016-07-08 13:27:02 -08:00
decorated = res;
} else {
notify('Plugin error!', `"${plugin._name}": invalid return type for \`${key}\``);
2016-07-08 13:27:02 -08:00
}
}
});
2016-07-08 13:27:02 -08:00
return decorated;
}
2018-04-16 06:17:17 -08:00
exports.getDeprecatedConfig = () => {
const deprecated = {};
const baseConfig = config.getConfig();
modules.forEach(plugin => {
2018-04-16 06:17:17 -08:00
if (!plugin.decorateConfig) {
return;
}
// We need to clone config in case of plugin modifies config directly.
let configTmp;
try {
configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig)));
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
error: e
});
2018-04-16 06:17:17 -08:00
return;
}
2018-04-16 06:17:17 -08:00
const pluginCSSDeprecated = config.getDeprecatedCSS(configTmp);
if (pluginCSSDeprecated.length === 0) {
return;
}
deprecated[plugin._name] = {css: pluginCSSDeprecated};
});
2018-04-16 06:17:17 -08:00
return deprecated;
};
2018-04-16 06:17:17 -08:00
exports.decorateMenu = tpl => {
return decorateObject(tpl, 'decorateMenu');
};
2018-04-16 06:17:17 -08:00
exports.getDecoratedEnv = baseEnv => {
return decorateObject(baseEnv, 'decorateEnv');
2016-07-07 16:16:44 -08:00
};
2018-04-16 06:17:17 -08:00
exports.getDecoratedConfig = () => {
const baseConfig = config.getConfig();
2018-04-16 06:17:17 -08:00
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
const fixedConfig = config.fixConfigDefaults(decoratedConfig);
const translatedConfig = config.htermConfigTranslate(fixedConfig);
return translatedConfig;
};
exports.getDecoratedKeymaps = () => {
const baseKeymaps = config.getKeymaps();
// Ensure that all keys are in an array and don't use deprecated key combination`
const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps'));
return decoratedKeymaps;
2016-07-07 16:16:44 -08:00
};
2018-04-16 06:17:17 -08:00
exports.getDecoratedBrowserOptions = defaults => {
return decorateObject(defaults, 'decorateBrowserOptions');
};
exports._toDependencies = toDependencies;