hyper/app/plugins.ts

447 lines
12 KiB
TypeScript
Raw Normal View History

2020-01-02 05:44:11 -09:00
import {app, dialog, BrowserWindow, App} from 'electron';
2019-11-28 05:17:01 -09:00
import {resolve, basename} from 'path';
import {writeFileSync} from 'fs';
import Config from 'electron-store';
import ms from 'ms';
import React from 'react';
import ReactDom from 'react-dom';
import * as config from './config';
import notify from './notify';
import {availableExtensions} from './plugins/extensions';
import {install} from './plugins/install';
import {plugs} from './config/paths';
import mapKeys from './utils/map-keys';
2020-04-27 05:32:08 -08:00
import {configOptions} from '../lib/config';
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();
2020-01-02 05:44:11 -09:00
function getId(plugins_: any) {
2016-07-07 16:16:44 -08:00
return JSON.stringify(plugins_);
}
2020-01-02 05:44:11 -09:00
const watchers: Function[] = [];
2016-07-07 16:16:44 -08:00
// 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/vercel/hyper/issues/619
function patchModuleLoad() {
2020-01-02 05:44:11 -09:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Module = require('module');
const originalLoad = Module._load;
2020-01-02 05:44:11 -09:00
Module._load = function _load(modulePath: string) {
// 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:
2020-01-02 05:44:11 -09:00
// eslint-disable-next-line prefer-rest-params
return originalLoad.apply(this, arguments);
}
};
}
function checkDeprecatedExtendKeymaps() {
2020-03-25 02:15:08 -08:00
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;
2020-01-02 05:44:11 -09:00
install((err: any) => {
2016-07-07 16:16:44 -08:00
updating = false;
if (err) {
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
2020-03-25 02:15:08 -08:00
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);
2020-03-25 02:15:08 -08:00
return paths_.map((path_) => {
2016-07-08 06:40:48 -08:00
let version = null;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
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
2020-03-25 02:15:08 -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];
}
}
}
2019-11-28 05:17:01 -09:00
export {updatePlugins};
2016-07-07 16:16:44 -08:00
2019-11-28 05:17:01 -09:00
export const getLoadedPluginVersions = () => {
2020-03-25 02:15:08 -08:00
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
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
2020-04-27 05:32:08 -08:00
setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins']));
2019-05-08 20:28:52 -08:00
}
})();
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',
repository: 'vercel/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}`);
}
}
2020-01-02 05:44:11 -09:00
function alert(message: string) {
2016-07-07 16:16:44 -08:00
dialog.showMessageBox({
message,
buttons: ['Ok']
});
}
2020-01-02 05:44:11 -09:00
function toDependencies(plugins_: {plugins: string[]}) {
const obj: Record<string, string> = {};
2020-03-25 02:15:08 -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;
}
2020-01-02 05:44:11 -09:00
export const subscribe = (fn: Function) => {
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 {
2020-03-25 02:15:08 -08:00
plugins: plugins.plugins.map((name) => {
return resolve(path, 'node_modules', name.split('#')[0]);
2016-07-07 16:16:44 -08:00
}),
2020-03-25 02:15:08 -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
2019-11-28 05:17:01 -09:00
export {getPaths};
2016-07-07 16:16:44 -08:00
2016-07-08 08:48:37 -08:00
// get paths from renderer
2019-11-28 05:17:01 -09:00
export const getBasePaths = () => {
return {path, localPath};
2016-07-08 08:48:37 -08:00
};
2020-01-02 05:44:11 -09:00
function requirePlugins(): any[] {
const {plugins: plugins_, localPlugins} = paths;
2016-07-07 16:16:44 -08:00
2020-01-02 05:44:11 -09:00
const load = (path_: string) => {
let mod: any;
2016-07-07 16:16:44 -08:00
try {
mod = require(path_);
2020-03-25 02:15:08 -08:00
const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key));
if (!exposed) {
2019-11-28 05:17:01 -09: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
mod._name = basename(path_);
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
mod._version = require(resolve(path_, 'package.json')).version;
} catch (err) {
console.warn(`No package.json found in ${path_}`);
}
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') {
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))
2020-03-25 02:15:08 -08:00
.filter((v) => Boolean(v));
2016-07-07 16:16:44 -08:00
}
2020-01-02 05:44:11 -09:00
export const onApp = (app_: App) => {
2020-03-25 02:15:08 -08:00
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
}
});
};
2020-01-02 05:44:11 -09:00
export const onWindowClass = (win: BrowserWindow) => {
2020-03-25 02:15:08 -08:00
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
});
}
}
});
};
2020-01-02 05:44:11 -09:00
export const onWindow = (win: BrowserWindow) => {
2020-03-25 02:15:08 -08:00
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
2020-01-02 05:44:11 -09:00
function decorateEntity(base: any, key: string, type: 'object' | 'function') {
let decorated = base;
2020-03-25 02:15:08 -08:00
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;
}
2020-04-27 05:32:08 -08:00
function decorateObject<T>(base: T, key: string): T {
return decorateEntity(base, key, 'object');
}
2020-01-02 05:44:11 -09:00
function decorateClass(base: any, key: string) {
return decorateEntity(base, key, 'function');
}
2019-11-28 05:17:01 -09:00
export const getDeprecatedConfig = () => {
2020-04-27 05:32:08 -08:00
const deprecated: Record<string, {css: string[]}> = {};
const baseConfig = config.getConfig();
2020-03-25 02:15:08 -08:00
modules.forEach((plugin) => {
if (!plugin.decorateConfig) {
return;
}
// We need to clone config in case of plugin modifies config directly.
2020-04-27 05:32:08 -08:00
let configTmp: configOptions;
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;
};
2020-01-02 05:44:11 -09:00
export const decorateMenu = (tpl: any) => {
return decorateObject(tpl, 'decorateMenu');
};
2020-01-02 05:44:11 -09:00
export const getDecoratedEnv = (baseEnv: Record<string, string>) => {
return decorateObject(baseEnv, 'decorateEnv');
2016-07-07 16:16:44 -08:00
};
2019-11-28 05:17:01 -09:00
export const 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
};
2019-11-28 05:17:01 -09:00
export const 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;
};
2020-01-02 05:44:11 -09:00
export const getDecoratedBrowserOptions = <T>(defaults: T): T => {
return decorateObject(defaults, 'decorateBrowserOptions');
};
2020-01-02 05:44:11 -09:00
export const decorateWindowClass = <T>(defaults: T): T => {
return decorateObject(defaults, 'decorateWindowClass');
};
2020-01-02 05:44:11 -09:00
export const decorateSessionOptions = <T>(defaults: T): T => {
return decorateObject(defaults, 'decorateSessionOptions');
};
2020-01-02 05:44:11 -09:00
export const decorateSessionClass = <T>(Session: T): T => {
return decorateClass(Session, 'decorateSessionClass');
};
2019-11-28 05:17:01 -09:00
export {toDependencies as _toDependencies};