chore: Plugin registration (#6623)
* first pass * test * test * priority * Reduce boilerplate further * Update server/utils/PluginManager.ts Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com> * fix: matchesNode error in destroyed editor transaction * fix: Individual imported files do not display source correctly in 'Insights' * chore: Add sleep before Slack notification * docs * fix: Error logged about missing plugin.json * Remove email template glob --------- Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>
This commit is contained in:
173
server/utils/PluginManager.ts
Normal file
173
server/utils/PluginManager.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import path from "path";
|
||||
import { glob } from "glob";
|
||||
import type Router from "koa-router";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { UnfurlSignature } from "@shared/types";
|
||||
import type BaseEmail from "@server/emails/templates/BaseEmail";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import type BaseProcessor from "@server/queues/processors/BaseProcessor";
|
||||
import type BaseTask from "@server/queues/tasks/BaseTask";
|
||||
|
||||
export enum PluginPriority {
|
||||
VeryHigh = 0,
|
||||
High = 100,
|
||||
Normal = 200,
|
||||
Low = 300,
|
||||
VeryLow = 500,
|
||||
}
|
||||
|
||||
/**
|
||||
* The different types of server plugins that can be registered.
|
||||
*/
|
||||
export enum PluginType {
|
||||
API = "api",
|
||||
AuthProvider = "authProvider",
|
||||
EmailTemplate = "emailTemplate",
|
||||
Processor = "processor",
|
||||
Task = "task",
|
||||
UnfurlProvider = "unfurl",
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of plugin types to their values, for example an API plugin would have a value of type
|
||||
* Router. Registering an API plugin causes the router to be mounted.
|
||||
*/
|
||||
type PluginValueMap = {
|
||||
[PluginType.API]: Router;
|
||||
[PluginType.AuthProvider]: Router;
|
||||
[PluginType.EmailTemplate]: typeof BaseEmail;
|
||||
[PluginType.Processor]: typeof BaseProcessor;
|
||||
[PluginType.Task]: typeof BaseTask<any>;
|
||||
[PluginType.UnfurlProvider]: UnfurlSignature;
|
||||
};
|
||||
|
||||
export type Plugin<T extends PluginType> = {
|
||||
/** A unique ID for the plugin */
|
||||
id: string;
|
||||
/** The plugin's display name */
|
||||
name?: string;
|
||||
/** A brief description of the plugin */
|
||||
description?: string;
|
||||
/** The plugin content */
|
||||
value: PluginValueMap[T];
|
||||
/** An optional priority, will affect order in menus and execution. Lower is earlier. */
|
||||
priority?: number;
|
||||
/** Whether the plugin is enabled (default: true) */
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export class PluginManager {
|
||||
private static plugins = new Map<PluginType, Plugin<PluginType>[]>();
|
||||
|
||||
/**
|
||||
* Register a plugin of a given type.
|
||||
*
|
||||
* @param type The plugin type
|
||||
* @param value The plugin value
|
||||
* @param options Additional options, including whether the plugin is enabled and it's priority.
|
||||
* @returns The PluginManager instance, for chaining.
|
||||
*/
|
||||
public static register<T extends PluginType>(
|
||||
type: T,
|
||||
value: PluginValueMap[T],
|
||||
options: Omit<Plugin<T>, "value"> = {
|
||||
id: uuid(),
|
||||
}
|
||||
) {
|
||||
if (!this.plugins.has(type)) {
|
||||
this.plugins.set(type, []);
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
value,
|
||||
priority: PluginPriority.Normal,
|
||||
...options,
|
||||
};
|
||||
|
||||
Logger.debug(
|
||||
"plugins",
|
||||
`Plugin ${options.enabled === false ? "disabled" : "enabled"} "${
|
||||
options.id
|
||||
}" ${options.description ? `(${options.description})` : ""}`
|
||||
);
|
||||
|
||||
this.plugins.get(type)!.push(plugin);
|
||||
|
||||
// allow chaining
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntactic sugar for registering a background Task.
|
||||
*
|
||||
* @param value The task class
|
||||
* @param options Additional options
|
||||
*/
|
||||
public static registerTask(
|
||||
value: PluginValueMap[PluginType.Task],
|
||||
options?: Omit<Plugin<PluginType.Task>, "id" | "value">
|
||||
) {
|
||||
return this.register(PluginType.Task, value, {
|
||||
id: value.name,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntactic sugar for registering a background Processor.
|
||||
*
|
||||
* @param value The processor class
|
||||
* @param options Additional options
|
||||
*/
|
||||
public static registerProcessor(
|
||||
value: PluginValueMap[PluginType.Processor],
|
||||
options?: Omit<Plugin<PluginType.Processor>, "id" | "value">
|
||||
) {
|
||||
return this.register(PluginType.Processor, value, {
|
||||
id: value.name,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the plugins of a given type in order of priority.
|
||||
*
|
||||
* @param type The type of plugin to filter by
|
||||
* @returns A list of plugins
|
||||
*/
|
||||
public static getPlugins<T extends PluginType>(type: T) {
|
||||
this.loadPlugins();
|
||||
return sortBy(this.plugins.get(type) || [], "priority") as Plugin<T>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the enabled plugins of a given type in order of priority.
|
||||
*
|
||||
* @param type The type of plugin to filter by
|
||||
* @returns A list of plugins
|
||||
*/
|
||||
public static getEnabledPlugins<T extends PluginType>(type: T) {
|
||||
return this.getPlugins(type).filter((plugin) => plugin.enabled !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin server components (anything in the `/server/` directory of a plugin will be loaded)
|
||||
*/
|
||||
public static loadPlugins() {
|
||||
if (this.loaded) {
|
||||
return;
|
||||
}
|
||||
const rootDir = env.ENVIRONMENT === "test" ? "" : "build";
|
||||
|
||||
glob
|
||||
.sync(path.join(rootDir, "plugins/*/server/!(*.test|schema).[jt]s"))
|
||||
.forEach((filePath: string) => {
|
||||
require(path.join(process.cwd(), filePath));
|
||||
});
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
private static loaded = false;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from "path";
|
||||
import glob from "glob";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { UnfurlResolver } from "@server/types";
|
||||
import environment from "./environment";
|
||||
|
||||
const rootDir = env.ENVIRONMENT === "test" ? "" : "build";
|
||||
|
||||
const plugins = glob.sync(path.join(rootDir, "plugins/*/server/unfurl.[jt]s"));
|
||||
const resolvers: Record<string, UnfurlResolver> = plugins.reduce(
|
||||
(resolvers, filePath) => {
|
||||
const resolver: UnfurlResolver = require(path.join(
|
||||
process.cwd(),
|
||||
filePath
|
||||
));
|
||||
const id = filePath.replace("build/", "").split("/")[1];
|
||||
const config = require(path.join(
|
||||
process.cwd(),
|
||||
rootDir,
|
||||
"plugins",
|
||||
id,
|
||||
"plugin.json"
|
||||
));
|
||||
|
||||
// Test the all required env vars are set for the resolver
|
||||
const enabled = (config.requiredEnvVars ?? []).every(
|
||||
(name: string) => !!environment[name]
|
||||
);
|
||||
if (!enabled) {
|
||||
return resolvers;
|
||||
}
|
||||
|
||||
resolvers[config.name] = resolver;
|
||||
Logger.debug("utils", `Registered unfurl resolver ${filePath}`);
|
||||
|
||||
return resolvers;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
export default resolvers;
|
||||
Reference in New Issue
Block a user