UNPKG

imdone-core

Version:
445 lines (394 loc) 12.6 kB
import Emitter from 'events' import Plugin from 'imdone-api' import _path from 'path' import { downloadPlugin } from '../adapters/git-download.js' import chokidar from 'chokidar' import debounce from 'lodash.debounce' import { exists, mkdir, readdir, rm } from '../adapters/file-gateway.js' import { appContext } from '../context/ApplicationContext.js' // import { createRequire } from 'node:module'; import PersistTagsPlugin from './persist-tags-plugin.js' import DefaultBoardPropertiesPlugin from './default-board-properties-plugin.js' import DefaultBoardActionsPlugin from './default-board-actions-plugin.js' import ArchivePlugin from './archive-plugin.js' import EpicPlugin from './epic-plugin.js' import ExtensionPlugin from './extension-plugin.js' import { logger } from '../adapters/logger.js' // import { URL } from 'node:url' // const require = createRequire(new URL(import.meta.url)); export class PluginManager extends Emitter { constructor(project) { super() this.project = project this.defaultPlugins = [ PersistTagsPlugin, DefaultBoardPropertiesPlugin, DefaultBoardActionsPlugin, ArchivePlugin, EpicPlugin, ExtensionPlugin, ] this.pluginsMap = {} this.pluginPath = _path.join(project.path, '.imdone', 'plugins') this.onDevChange = debounce(this.onDevChange.bind(this), 1000) } async startDevMode() { if (this.project && this.project.config.devMode && !this.watcher) { if (!(await exists(this.pluginPath))) await mkdir(this.pluginPath) this.watcher = chokidar(this.pluginPath, { ignored(path) { return /node_modules/.test(path) }, }) this.watcher.on('change', (path, root, stat) => { this.onDevChange() }) this.watcher.on('add', (path, root, stat) => { this.onDevChange() }) } } stopDevMode() { this.watcher && this.watcher.close() this.watcher = null } initDevMode() { if (!this.project.config.devMode) this.stopDevMode() this.startDevMode() } async onDevChange() { try { await this.reloadPlugins() this.emit('plugins-reloaded') } catch { logger.error('Error reloading plugins.', err) throw err } } async reloadPlugins() { this.destroyPlugins() await this.loadPlugins() await this.startDevMode() } async uninstallPlugin(pluginName) { logger.log('Preparing to uninstall plugin:', pluginName) const pluginClassName = Object.keys(this.pluginsMap).find((key) => { return this.getPlugin(key).info.name === pluginName }) if (!pluginClassName) { throw new Error('Unable to find plugin:' + pluginName) } const { info } = this.getPlugin(pluginClassName) const { path } = info // const pkg = { version, name } logger.log('Uninstalling:', info) await rm(path, { recursive: true, force: true }) delete this.pluginsMap[pluginClassName] this.emit('plugin-uninstalled', pluginName) } async installPlugin({ name, version }) { if (!(await exists(this.pluginPath))) await mkdir(this.pluginPath) const installPath = _path.join(this.pluginPath, name) await downloadPlugin(version, installPath) logger.log(`Done installing ${name}`) await this.loadPlugin(installPath) this.emit('plugin-installed', name) } async loadPlugins() { for (const PluginClass of this.defaultPlugins) { await this.createPlugin(PluginClass) } await this.loadInstalledPlugins() await this.loadPluginsNotInstalled() } async loadInstalledPlugins() { const path = await exists(this.pluginPath) if (!path) { await mkdir(this.pluginPath) } const paths = await readdir(this.pluginPath, { withFileTypes: true }) const pluginPaths = paths .filter( (entry) => entry.name !== 'node_modules' && (entry.isDirectory() || entry.isSymbolicLink()) ) .map((entry) => _path.join(this.pluginPath, entry.name)) for (const path of pluginPaths) { await this.loadPlugin(path) } } async loadPluginsNotInstalled() { const availablePlugins = await appContext().pluginRegistry.getAvailablePlugins() const configPluginNames = Object.keys(this.project.config.plugins) if (!configPluginNames) return const installedPluginNames = Object.keys(this.pluginsMap) const pluginsNotInstalled = configPluginNames.filter(name => !installedPluginNames.includes(name)) for (const pluginName of pluginsNotInstalled) { const plugin = availablePlugins.find(p => p.name === pluginName) if (plugin && plugin.name) await this.installPlugin(plugin) } } async loadPlugin(path) { logger.log('Loading plugin: ', path) const fullPath = path.endsWith('.js') ? _path.resolve(path) : _path.join(_path.resolve(path), 'bundle.js') try { // eslint-disable-next-line delete require.cache[require.resolve(fullPath)] // eslint-disable-next-line const pluginClass = await import(_path.resolve(path)) const pluginInstance = await this.createPlugin(pluginClass.default, path) return pluginInstance } catch (e) { logger.error(`Error loading plugin at: ${path}`, e) } } async getPackageInfo(path) { if (!path) return {} let info = { path } const packagePath = _path.join(path, 'package.json') const packageInfo = await import(packagePath) try { info = {...info, ...packageInfo } delete info.dependencies delete info.devDependencies delete info.scripts delete info.main } catch (e) { logger.info('No info on plugin:', path) } } async createPlugin(pluginClass, path = undefined) { const name = pluginClass.pluginName if (!name) { throw new Error(`${pluginClass.name} is not a plugin`) } const pluginInstance = new pluginClass(this.project) const packageInfo = await this.getPackageInfo(path) let info = { name, ...packageInfo, } pluginInstance.getSettings = () => { return this.getPluginSettings(name) } this.pluginsMap[name] = { pluginInstance, pluginClass, info, } if (pluginInstance.init) await pluginInstance.init() return pluginInstance } destroyPlugins() { this.stopDevMode() this.eachPlugin(({ pluginInstance }) => { try { pluginInstance.destroy() } catch (e) { this.pluginError('destroy', pluginInstance, e) } }) } eachPlugin(cb) { Object.keys(this.pluginsMap).forEach((key) => { cb(this.getPlugin(key)) }) } async eachPluginAsync(cb) { for (const key of Object.keys(this.pluginsMap)) { await cb(this.getPlugin(key)) } } getPlugins() { return Object.keys(this.pluginsMap).map((key) => { const { info, pluginInstance } = this.getPlugin(key) const schema = pluginInstance.getSettingsSchema() if (schema) { schema.id = info.name schema.title = `${info.name} settings` } return { ...info, schema } }) } disablePlugin(name) { delete this.pluginsMap[name] } getPluginName(pluginInstance) { return Object.keys(this.pluginsMap).find( (pluginName) => this.getPlugin(pluginName).pluginInstance === pluginInstance ) } getPluginInstance(name) { const plugin = this.getPlugin(name) if (!plugin) throw new Error(`Plugin ${name} not found`) return plugin.pluginInstance } getPlugin(name) { return this.pluginsMap[name] } getPluginSettings(name) { return this.project.config.plugins[name] || {} } pluginError(method, pluginInstance, error) { logger.warn( `Plugin: ${this.getPluginName( pluginInstance )} threw an error on ${method}: `, error ) } async onBoardUpdate(lists) { if (!lists || lists.length == 0) return await this.eachPluginAsync(async ({ pluginInstance }) => { // const timeLabel = `${this.getPluginName(pluginInstance)} onBoardUpdate time` // logger.time(timeLabel) try { await pluginInstance.onBoardUpdate(lists) } catch (e) { this.pluginError('onBoardUpdate', pluginInstance, e) } // logger.timeEnd(timeLabel) }) return lists } async onBeforeBoardUpdate() { await this.eachPluginAsync(async ({ pluginInstance }) => { try { await pluginInstance.onBeforeBoardUpdate() } catch (e) { this.pluginError('onBeforeBoardUpdate', pluginInstance, e) } }) } onTaskUpdate(task) { this.eachPlugin(({ pluginInstance }) => { try { pluginInstance.onTaskUpdate(task) } catch (e) { this.pluginError('onTaskUpdate', pluginInstance, e) } }) } async onTaskFound(task) { await this.eachPluginAsync(async ({ pluginInstance }) => { try { if (pluginInstance.onTaskFound) await pluginInstance.onTaskFound(task) } catch (e) { this.pluginError('onTaskFound', pluginInstance, e) } }) } async onBeforeAddTask({path, list, content, tags, contexts, meta, useCardTemplate}) { await this.eachPluginAsync(async ({ pluginInstance }) => { try { const pluginMods = await pluginInstance.onBeforeAddTask({path, list, content, tags, contexts, meta, useCardTemplate}) path = pluginMods.path content = pluginMods.content tags = pluginMods.tags contexts = pluginMods.contexts meta = pluginMods.meta } catch (e) { this.pluginError('onBeforeAddTask', pluginInstance, e) } }) return {path, content, tags, contexts, meta} } async onAfterDeleteTask(task) { await this.eachPluginAsync(async ({ pluginInstance }) => { try { await pluginInstance.onAfterDeleteTask(task) } catch (e) { this.pluginError('onAfterDeleteTask', pluginInstance, e) } }) } getCardProperties(props) { let cardProps = {} this.eachPlugin(({ pluginInstance }) => { try { cardProps = { ...cardProps, ...pluginInstance.getCardProperties(props) } } catch (e) { this.pluginError('getCardProperties', pluginInstance, e) } }) return cardProps } async getBoardProperties() { let boardProps = {} await this.eachPluginAsync(async ({ pluginInstance }) => { try { const pluginProps = pluginInstance.getBoardProperties ? await pluginInstance.getBoardProperties() : {} boardProps = { ...boardProps, ...pluginProps } } catch (e) { this.pluginError('getBoardProperties', pluginInstance, e) } }) return boardProps } getCardActions(task) { let cardLinks = [] this.eachPlugin(({ pluginInstance }) => { try { cardLinks = [ ...cardLinks, ...pluginInstance.getCardActions(task).map((link, index) => { return { ...link, action: { plugin: this.getPluginName(pluginInstance), index }, } }), ] } catch (e) { this.pluginError('getCardActions', pluginInstance, e) } }) return cardLinks } getBoardActions() { let actions = [] this.eachPlugin(({ pluginInstance }) => { try { actions = [ ...actions, ...pluginInstance.getBoardActions().map((item, index) => { if (item.title) item.name = item.title return { ...item, plugin: this.getPluginName(pluginInstance), index, } }), ] } catch (e) { this.pluginError('getBoardActions', pluginInstance, e) } }) return actions } performCardAction(action, task) { const plugin = this.getPluginInstance(action.plugin) try { return plugin.getCardActions(task)[action.index].action() } catch (e) { this.pluginError('getCardActions', plugin, e) } } async performBoardAction(action, task) { const title = action.title || action.name const plugin = this.getPluginInstance(action.plugin) try { return action.index ? await plugin.getBoardActions()[action.index].action(task) : await plugin.getBoardActions().find(a => a.title === title).action(task) } catch (e) { this.pluginError('getBoardActions', plugin, e) throw new Error(`Error performing action ${title} on plugin ${plugin.name}: ${e.message}`) } } }