🏡 index : quine-global/hyper.git

import { ipcRenderer, remote } from 'electron';
import { connect as reduxConnect } from 'react-redux';

// we expose these two deps to component decorators
import React from 'react';
import Notification from '../components/notification';
import notify from './notify';

// remote interface to `../plugins`
let plugins = remote.require('./plugins');

// `require`d modules
let modules;

// cache of decorated components
let decorated = {};

// varios caches extracted of the plugin methods
let connectors;
let middlewares;
let uiReducers;
let sessionsReducers;
let tabPropsDecorators;
let tabsPropsDecorators;
let termPropsDecorators;

// the fs locations where usr plugins are stored
const { path, localPath } = plugins.getBasePaths();

const clearModulesCache = () => {
  // trigger unload hooks
  modules.forEach((mod) => {
    if (mod.onRendererUnload) mod.onRendererUnload(window);
  });

  // clear require cache
  for (const entry in window.require.cache) {
    if (entry.indexOf(path) === 0 || entry.indexOf(localPath) === 0) {
      // `require` is webpacks', `window.require`, electron's
      delete window.require.cache[entry];
    }
  }
};

const getPluginName = (path) => window.require('path').basename(path);

const loadModules = () => {
  console.log('(re)loading renderer plugins');
  const paths = plugins.getPaths();

  // initialize cache that we populate with extension methods
  connectors = {
    Terms: { state: [], dispatch: [] },
    Header: { state: [], dispatch: [] },
    HyperTerm: { state: [], dispatch: [] },
    Notifications: { state: [], dispatch: [] }
  };
  uiReducers = [];
  middlewares = [];
  sessionsReducers = [];
  tabPropsDecorators = [];
  tabsPropsDecorators = [];
  termPropsDecorators = [];

  modules = paths.plugins.concat(paths.localPlugins)
  .map((path) => {
    let mod;
    const pluginName = getPluginName(path);

    // window.require allows us to ensure this doens't get
    // in the way of our build
    try {
      mod = window.require(path);
    } catch (err) {
      console.error(err.stack);
      notify('Plugin load error', `"${pluginName}" failed to load in the renderer process. Check Developer Tools for details.`);
      return;
    }

    for (const i in mod) {
      mod[i]._pluginName = pluginName;
    }

    if (mod.middleware) {
      middlewares.push(mod.middleware);
    }

    if (mod.reduceUI) {
      uiReducers.push(mod.reduceUI);
    }

    if (mod.reduceSessions) {
      sessionsReducers.push(mod.reduceSessions);
    }

    if (mod.mapTermsState) {
      connectors.Terms.state.push(mod.mapTermsState);
    }

    if (mod.mapTermsDispatch) {
      connectors.Terms.dispatch.push(mod.mapTermsState);
    }

    if (mod.mapHeaderState) {
      connectors.Header.state.push(mod.mapHeaderState);
    }

    if (mod.mapHeaderDispatch) {
      connectors.Header.dispatch.push(mod.mapHeaderDispatch);
    }

    if (mod.mapHyperTermState) {
      connectors.HyperTerm.state.push(mod.mapHyperTermState);
    }

    if (mod.mapHyperTermDispatch) {
      connectors.HyperTerm.dispatch.push(mod.mapHyperTermDispatch);
    }

    if (mod.mapNotificationsState) {
      connectors.Notifications.state.push(mod.mapNotificationsState);
    }

    if (mod.mapNotificationsDispatch) {
      connectors.Notifications.dispatch.push(mod.mapNotificationsDispatch);
    }

    if (mod.getTermProps) {
      termPropsDecorators.push(mod.getTermProps);
    }

    if (mod.getTabProps) {
      tabPropsDecorators.push(mod.getTabProps);
    }

    if (mod.getTabsProps) {
      tabsPropsDecorators.push(mod.getTabsProps);
    }

    if (mod.onRendererWindow) {
      mod.onRendererWindow(window);
    }

    return mod;
  })
  .filter((mod) => !!mod);
};

// load modules for initial decoration
loadModules();

export function reload () {
  clearModulesCache();
  loadModules();
  // trigger re-decoration when components
  // get re-rendered
  decorated = {};
}

export function getTermProps (uid, parentProps, props) {
  let props_;

  termPropsDecorators.forEach((fn) => {
    let ret_;

    if (!props_) props_ = Object.assign({}, props);

    try {
      ret_ = fn(uid, parentProps, props_);
    } catch (err) {
      console.error(err.stack);
      notify('Plugin error', `${fn._pluginName}: Error occurred in \`getTermProps\`. Check Developer Tools for details.`);
      return;
    }

    if (!ret_ || 'object' !== typeof ret_) {
      notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTermProps\` (object expected).`);
      return;
    }

    props = ret_;
  });

  return props_ || props;
}

export function getTabsProps (parentProps, props) {
  let props_;

  tabsPropsDecorators.forEach((fn) => {
    let ret_;

    if (!props_) props_ = Object.assign({}, props);

    try {
      ret_ = fn(parentProps, props_);
    } catch (err) {
      console.error(err.stack);
      notify('Plugin error', `${fn._pluginName}: Error occurred in \`getTabsProps\`. Check Developer Tools for details.`);
      return;
    }

    if (!ret_ || 'object' !== typeof ret_) {
      notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabsProps\` (object expected).`);
      return;
    }

    props_ = ret_;
  });

  return props_ || props;
}

export function getTabProps (tab, parentProps, props) {
  let props_;

  tabPropsDecorators.forEach((fn) => {
    let ret_;

    if (!props_) props_ = Object.assign({}, props);

    try {
      ret_ = fn(tab, parentProps, props_);
    } catch (err) {
      console.error(err.stack);
      notify('Plugin error', `${fn._pluginName}: Error occurred in \`getTabProps\`. Check Developer Tools for details.`);
      return;
    }

    if (!ret_ || 'object' !== typeof ret_) {
      notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabProps\` (object expected).`);
      return;
    }

    props_ = ret_;
  });

  return props_ || props;
}

// connects + decorates a class
// plugins can override mapToState, dispatchToProps
// and the class gets deorated (proxied)
export function connect (stateFn, dispatchFn, c, d = {}) {
  return function (Class, name) {
    return reduxConnect(
      function (state) {
        let ret = stateFn(state);
        connectors[name].state.forEach((fn) => {
          let ret_;

          try {
            ret_ = fn(state, ret);
          } catch (err) {
            console.error(err.stack);
            notify('Plugin error', `${fn._pluginName}: Error occurred in \`map${name}State\`. Check Developer Tools for details.`);
            return;
          }

          if (!ret_ || 'object' !== typeof ret_) {
            notify('Plugin error', `${fn._pluginName}: Invalid return value of \`map${name}State\` (object expected).`);
            return;
          }

          ret = ret_;
        });
        return ret;
      },
      dispatchFn,
      c,
      d
    )(decorate(Class, name));
  };
}

export function decorateUIReducer (fn) {
  return (state, action) => {
    let state_ = fn(state, action);

    uiReducers.forEach((pluginReducer) => {
      let state__;

      try {
        state__ = pluginReducer(state_, action);
      } catch (err) {
        console.error(err.stack);
        notify('Plugin error', `${fn._pluginName}: Error occurred in \`reduceUI\`. Check Developer Tools for details.`);
        return;
      }

      if (!state__ || 'object' !== typeof state__) {
        notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceUI\`.`);
        return;
      }

      state_ = state__;
    });

    return state_;
  };
}

export function decorateSessionsReducer (fn) {
  return (state, action) => {
    let state_ = fn(state, action);

    sessionsReducers.forEach((pluginReducer) => {
      let state__;

      try {
        state__ = pluginReducer(state_, action);
      } catch (err) {
        console.error(err.stack);
        notify('Plugin error', `${fn._pluginName}: Error occurred in \`reduceSessions\`. Check Developer Tools for details.`);
        return;
      }

      if (!state__ || 'object' !== typeof state__) {
        notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceSessions\`.`);
        return;
      }

      state_ = state__;
    });

    return state_;
  };
}

// redux middleware generator
export const middleware = (store) => (next) => (action) => {
  const nextMiddleware = remaining => action => remaining.length
    ? remaining[0](store)(nextMiddleware(remaining.slice(1)))(action)
    : next(action);
  nextMiddleware(middlewares)(action);
};

function getDecorated (parent, name) {
  if (!decorated[name]) {
    let class_ = parent;

    modules.forEach((mod) => {
      const method = 'decorate' + name;
      const fn = mod[method];

      if (fn) {
        let class__;

        try {
          class__ = fn(class_, { React, Notification, notify });
        } catch (err) {
          console.error(err.stack);
          notify('Plugin error', `${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`);
          return;
        }

        if (!class__ || 'function' !== typeof class__.prototype.render) {
          notify('Plugin error', `${fn._pluginName}: Invalid return value of \`${method}\`. No \`render\` method found. Please return a \`React.Component\`.`);
          return;
        }

        class_ = class__;
      }
    });

    decorated[name] = class_;
  }

  return decorated[name];
}

// for each component, we return a higher-order component
// that wraps with the higher-order components
// exposed by plugins
export function decorate (Component, name) {
  return class extends React.Component {
    render () {
      const Sub = getDecorated(Component, name);
      return <Sub {...this.props} />;
    }
  };
}