hyper/app/plugins.js

449 lines
12 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');
Update Electron to v6 (#3785) * 3.0.0 * 3.0.2 * Save * Save * Upgrade yarn lock packages * update node-gyp and node-pty * update travis and appveyor to node 12 * appveyor is outdated as always * update travis to xenial * update node-pty@0.9.0-beta26 * update yarn.lock * update electron to 6.0.8 * move node-pty to the correct package.json * Fix linting failure * Update yarn lockfile to try to fix appveyor build * Remove unnecessary changes from package.json * Try to fix appveyor by using a newer image * Fix linting after my last change * update electron to 6.0.9 * install windows-build-tools on appveyor * fix syntax * switch back to 2017 image * remove old resolutions field * revert accidental version change * update electron to 6.0.11 and electron-rebuild to 1.8.6 * downgrade yarn to 1.18 until this issue is resolved https://github.com/yarnpkg/yarn/issues/7584 * update node-gyp to 6.0.0 and generate a fresh yarn lockfile * update react and a few other dependencies * fix lint * this should actually be electron-builder, I think! * update a few dependencies * change to electron-store electron-config was renamed to electron-store a while ago * update xterm to v4.1.0 and ora to 4.0.2 * move pify to app/package.json * TODO: Revert maybe. Throw a fit on every change to maybe fix the resizing issues * a * fix react ref problem * fix split view focus problem * remove the unnecessary fit * remove the init col and row * fix the problem that cannot show about hyper * update electron to 6.0.12 * fix lint * add more todos for componentWillReceiveProps deprecation * update babel and plugins Co-authored-by: Juan Campa <juancampa@gmail.com> Co-authored-by: Benjamin Staneck <staneck@gmail.com> Co-authored-by: ivan <ivanwonder@outlook.com>
2019-10-10 11:20:26 -08:00
const Config = require('electron-store');
2016-07-07 16:16:44 -08:00
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');
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();
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);
}
};
}
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) {
//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
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
watchers.forEach(fn => fn(err, {force}));
2016-07-13 12:44:24 -08:00
if (force || changed) {
if (changed) {
notify('Plugins Updated', 'Restart the app or hot-reload with "View" > "Reload" to enjoy the updates!');
} else {
notify('Plugins Updated', 'No changes!');
}
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);
return paths_.map(path_ => {
2016-07-08 06:40:48 -08:00
let version = null;
try {
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;
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
//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();
2017-11-08 12:24:15 -09:00
}, 1000);
2016-07-07 16:16:44 -08:00
}
2019-05-08 20:28:52 -08:00
(() => {
const baseConfig = config.getConfig();
if (baseConfig['autoUpdatePlugins']) {
// otherwise update plugins every 5 hours
setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins']));
}
})();
2016-07-07 16:16:44 -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}`);
}
}
function alert(message) {
2016-07-07 16:16:44 -08:00
dialog.showMessageBox({
message,
buttons: ['Ok']
});
}
function toDependencies(plugins_) {
2016-07-07 16:16:44 -08:00
const obj = {};
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;
}
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 => {
return resolve(path, 'node_modules', name.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
exports.getBasePaths = () => {
return {path, localPath};
2016-07-08 08:48:37 -08:00
};
function requirePlugins() {
const {plugins: plugins_, localPlugins} = paths;
2016-07-07 16:16:44 -08:00
const load = path_ => {
2016-07-07 16:16:44 -08:00
let mod;
try {
mod = require(path_);
const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key));
if (!exposed) {
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
mod._name = basename(path_);
try {
mod._version = require(resolve(path_, 'package.json')).version;
} catch (err) {
//eslint-disable-next-line no-console
console.warn(`No package.json found in ${path_}`);
}
//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) {
2017-11-08 12:24:15 -09:00
if (err.code === 'MODULE_NOT_FOUND') {
//eslint-disable-next-line no-console
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
2017-11-08 12:24:15 -09:00
} else {
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err});
2017-11-08 12:24:15 -09:00
}
2016-07-07 16:16:44 -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
}
exports.onApp = app_ => {
modules.forEach(plugin => {
2016-07-07 16:16:44 -08:00
if (plugin.onApp) {
try {
plugin.onApp(app_);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
error: e
});
}
2016-07-07 16:16:44 -08:00
}
});
};
exports.onWindowClass = win => {
modules.forEach(plugin => {
if (plugin.onWindowClass) {
try {
plugin.onWindowClass(win);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
error: e
});
}
}
});
};
exports.onWindow = win => {
modules.forEach(plugin => {
2016-07-07 16:16:44 -08:00
if (plugin.onWindow) {
try {
plugin.onWindow(win);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
error: e
});
}
2016-07-07 16:16:44 -08:00
}
});
};
// decorates the base entity by calling plugin[key]
// for all the available plugins
function decorateEntity(base, key, type) {
let decorated = base;
modules.forEach(plugin => {
if (plugin[key]) {
let res;
try {
res = plugin[key](decorated);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" when decorating ${key}`, {error: e});
return;
}
if (res && (!type || typeof res === type)) {
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;
}
function decorateObject(base, key) {
return decorateEntity(base, key, 'object');
}
function decorateClass(base, key) {
return decorateEntity(base, key, 'function');
}
exports.getDeprecatedConfig = () => {
const deprecated = {};
const baseConfig = config.getConfig();
modules.forEach(plugin => {
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
});
return;
}
const pluginCSSDeprecated = config.getDeprecatedCSS(configTmp);
if (pluginCSSDeprecated.length === 0) {
return;
}
deprecated[plugin._name] = {css: pluginCSSDeprecated};
});
return deprecated;
};
exports.decorateMenu = tpl => {
return decorateObject(tpl, 'decorateMenu');
};
exports.getDecoratedEnv = baseEnv => {
return decorateObject(baseEnv, 'decorateEnv');
2016-07-07 16:16:44 -08:00
};
exports.getDecoratedConfig = () => {
const baseConfig = config.getConfig();
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
const fixedConfig = config.fixConfigDefaults(decoratedConfig);
const translatedConfig = config.htermConfigTranslate(fixedConfig);
return translatedConfig;
2016-07-07 16:16:44 -08:00
};
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;
};
exports.getDecoratedBrowserOptions = defaults => {
return decorateObject(defaults, 'decorateBrowserOptions');
};
exports.decorateWindowClass = defaults => {
return decorateObject(defaults, 'decorateWindowClass');
};
exports.decorateSessionOptions = defaults => {
return decorateObject(defaults, 'decorateSessionOptions');
};
exports.decorateSessionClass = Session => {
return decorateClass(Session, 'decorateSessionClass');
};
exports._toDependencies = toDependencies;