import {DirectoryScanEntry, LocalPlugin} from 'app/common/plugin'; import log from 'app/server/lib/log'; import {readManifest} from 'app/server/lib/manifest'; import {getAppPathTo} from 'app/server/lib/places'; import * as fse from 'fs-extra'; import * as path from 'path'; /** * Various plugins' related directories. */ export interface PluginDirectories { /** * Directory where built in plugins are located. */ readonly builtIn?: string; /** * Directory where user installed plugins are localted. */ readonly installed?: string; } /** * * The plugin manager class is responsible for providing both built in and installed plugins and * spawning server side plugins's. * * Usage: * * const pluginManager = new PluginManager(appRoot, userRoot); * await pluginManager.initialize(); * */ export class PluginManager { public pluginsLoaded: Promise; // ========== Instance members and methods ========== private _dirs: PluginDirectories; private _validPlugins: LocalPlugin[] = []; private _entries: DirectoryScanEntry[] = []; /** * @param {string} userRoot: path to user's grist directory; `null` is allowed, to only uses built in plugins. * */ public constructor(public appRoot?: string, userRoot?: string) { this._dirs = { installed: userRoot ? path.join(userRoot, 'plugins') : undefined, builtIn: appRoot ? getAppPathTo(appRoot, 'plugins') : undefined }; } public dirs(): PluginDirectories { return this._dirs; } /** * Create tmp dir and load plugins. */ public async initialize(): Promise { try { await (this.pluginsLoaded = this.loadPlugins()); } catch (err) { log.error("PluginManager's initialization failed: ", err); throw err; } } /** * Re-load plugins (literally re-run `loadPlugins`). */ // TODO: it's not clear right now what we do on reload. Do we deactivate plugins that were removed // from the fs? Do we update plugins that have changed on the fs ? public async reloadPlugins(): Promise { return await this.loadPlugins(); } /** * Discover both builtIn and user installed plugins. Logs any failures that happens when scanning * a directory (ie: manifest missing or manifest validation errors etc...) */ public async loadPlugins(): Promise { this._entries = []; // Load user installed plugins if (this._dirs.installed) { this._entries.push(...await scanDirectory(this._dirs.installed, "installed")); } // Load builtIn plugins if (this._dirs.builtIn) { this._entries.push(...await scanDirectory(this._dirs.builtIn, "builtIn")); } if (!process.env.GRIST_EXPERIMENTAL_PLUGINS || process.env.GRIST_EXPERIMENTAL_PLUGINS === '0') { // Remove experimental plugins this._entries = this._entries.filter(entry => { if (entry.manifest && entry.manifest.experimental) { log.warn("Ignoring experimental plugin %s", entry.id); return false; } return true; }); } this._validPlugins = this._entries.filter(entry => !entry.errors).map(entry => entry as LocalPlugin); this._logScanningReport(); } public getPlugins(): LocalPlugin[] { return this._validPlugins; } private _logScanningReport() { const invalidPlugins = this._entries.filter( entry => entry.errors); if (invalidPlugins.length) { for (const plugin of invalidPlugins) { log.warn(`Error loading plugins: Failed to load extension from ${plugin.path}\n` + (plugin.errors!).map(m => " - " + m).join("\n ") ); } } log.info(`Found ${this._validPlugins.length} valid plugins on the system`); for (const p of this._validPlugins) { log.debug("PLUGIN %s -- %s", p.id, p.path); } } } async function scanDirectory(dir: string, kind: "installed"|"builtIn"): Promise { const plugins: DirectoryScanEntry[] = []; let listDir; try { listDir = await fse.readdir(dir); } catch (e) { // Non existing dir is treated as an empty dir. // It is hard for user to avoid Grist checking a dir, // so phrase the message as information rather than error. log.info(`No plugins found in directory: ${dir}`); return []; } for (const id of listDir) { const folderPath = path.join(dir, id), plugin: DirectoryScanEntry = { path: folderPath, id: `${kind}/${id}` }; try { plugin.manifest = await readManifest(folderPath); } catch (e) { plugin.errors = []; if (e.message) { plugin.errors.push(e.message); } if (e.notices) { plugin.errors.push(...e.notices); } } plugins.push(plugin); } return plugins; }