2020-07-21 13:20:51 +00:00
|
|
|
import {DirectoryScanEntry, LocalPlugin} from 'app/common/plugin';
|
2022-07-04 14:14:55 +00:00
|
|
|
import log from 'app/server/lib/log';
|
2020-07-21 13:20:51 +00:00
|
|
|
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;
|
|
|
|
/**
|
2023-11-28 14:28:15 +00:00
|
|
|
* Directory where user installed plugins are located.
|
2020-07-21 13:20:51 +00:00
|
|
|
*/
|
|
|
|
readonly installed?: string;
|
2023-11-28 14:28:15 +00:00
|
|
|
/**
|
|
|
|
* Yet another option, for plugins that are included
|
|
|
|
* during a build but not part of the codebase itself.
|
|
|
|
*/
|
|
|
|
readonly bundled?: string;
|
2020-07-21 13:20:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* 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<void>;
|
|
|
|
|
|
|
|
// ========== 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.
|
|
|
|
*
|
|
|
|
*/
|
2023-11-28 14:28:15 +00:00
|
|
|
public constructor(public appRoot?: string, userRoot?: string,
|
|
|
|
public bundledRoot?: string) {
|
2020-07-21 13:20:51 +00:00
|
|
|
this._dirs = {
|
|
|
|
installed: userRoot ? path.join(userRoot, 'plugins') : undefined,
|
2023-11-28 14:28:15 +00:00
|
|
|
builtIn: appRoot ? getAppPathTo(appRoot, 'plugins') : undefined,
|
|
|
|
bundled: bundledRoot ? getAppPathTo(bundledRoot, 'plugins') : undefined,
|
2020-07-21 13:20:51 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-05-23 17:43:11 +00:00
|
|
|
public dirs(): PluginDirectories { return this._dirs; }
|
2020-07-21 13:20:51 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create tmp dir and load plugins.
|
|
|
|
*/
|
|
|
|
public async initialize(): Promise<void> {
|
|
|
|
try {
|
|
|
|
await (this.pluginsLoaded = this.loadPlugins());
|
|
|
|
} catch (err) {
|
|
|
|
log.error("PluginManager's initialization failed: ", err);
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-19 09:46:49 +00:00
|
|
|
* Re-load plugins (literally re-run `loadPlugins`).
|
2020-07-21 13:20:51 +00:00
|
|
|
*/
|
|
|
|
// 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<void> {
|
|
|
|
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<void> {
|
|
|
|
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"));
|
|
|
|
}
|
|
|
|
|
2023-11-28 14:28:15 +00:00
|
|
|
// Load bundled plugins
|
|
|
|
if (this._dirs.bundled) {
|
|
|
|
this._entries.push(...await scanDirectory(this._dirs.bundled, "bundled"));
|
|
|
|
}
|
|
|
|
|
2020-07-21 13:20:51 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-11-28 14:28:15 +00:00
|
|
|
async function scanDirectory(dir: string, kind: "installed"|"builtIn"|"bundled"): Promise<DirectoryScanEntry[]> {
|
2020-07-21 13:20:51 +00:00
|
|
|
const plugins: DirectoryScanEntry[] = [];
|
|
|
|
let listDir;
|
|
|
|
|
|
|
|
try {
|
|
|
|
listDir = await fse.readdir(dir);
|
|
|
|
} catch (e) {
|
2023-10-27 19:34:42 +00:00
|
|
|
// 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}`);
|
2020-07-21 13:20:51 +00:00
|
|
|
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;
|
|
|
|
}
|