256 lines
8.0 KiB
TypeScript
256 lines
8.0 KiB
TypeScript
import { engineConfig } from '@alilc/lowcode-editor-core';
|
|
import { getLogger } from '@alilc/lowcode-utils';
|
|
import {
|
|
ILowCodePluginRuntime,
|
|
ILowCodePluginManager,
|
|
IPluginContextOptions,
|
|
PluginPreference,
|
|
ILowCodePluginContextApiAssembler,
|
|
} from './plugin-types';
|
|
import { filterValidOptions, isLowCodeRegisterOptions } from './plugin-utils';
|
|
import { LowCodePluginRuntime } from './plugin';
|
|
// eslint-disable-next-line import/no-named-as-default
|
|
import LowCodePluginContext from './plugin-context';
|
|
import { invariant } from '../utils';
|
|
import sequencify from './sequencify';
|
|
import semverSatisfies from 'semver/functions/satisfies';
|
|
import {
|
|
IPublicTypePluginRegisterOptions,
|
|
IPublicTypePreferenceValueType,
|
|
IPublicTypePlugin,
|
|
} from '@alilc/lowcode-types';
|
|
|
|
const logger = getLogger({ level: 'warn', bizName: 'designer:pluginManager' });
|
|
|
|
// 保留的事件前缀
|
|
const RESERVED_EVENT_PREFIX = [
|
|
'designer',
|
|
'editor',
|
|
'skeleton',
|
|
'renderer',
|
|
'render',
|
|
'utils',
|
|
'plugin',
|
|
'engine',
|
|
'editor-core',
|
|
'engine-core',
|
|
'plugins',
|
|
'event',
|
|
'events',
|
|
'log',
|
|
'logger',
|
|
'ctx',
|
|
'context',
|
|
];
|
|
|
|
export class LowCodePluginManager implements ILowCodePluginManager {
|
|
private plugins: ILowCodePluginRuntime[] = [];
|
|
|
|
pluginsMap: Map<string, ILowCodePluginRuntime> = new Map();
|
|
pluginContextMap: Map<string, LowCodePluginContext> = new Map();
|
|
|
|
private pluginPreference?: PluginPreference = new Map();
|
|
|
|
contextApiAssembler: ILowCodePluginContextApiAssembler;
|
|
|
|
constructor(
|
|
contextApiAssembler: ILowCodePluginContextApiAssembler,
|
|
readonly viewName = 'global',
|
|
) {
|
|
this.contextApiAssembler = contextApiAssembler;
|
|
}
|
|
|
|
_getLowCodePluginContext = (options: IPluginContextOptions) => {
|
|
const { pluginName } = options;
|
|
let context = this.pluginContextMap.get(pluginName);
|
|
if (!context) {
|
|
context = new LowCodePluginContext(options, this.contextApiAssembler);
|
|
this.pluginContextMap.set(pluginName, context);
|
|
}
|
|
return context;
|
|
};
|
|
|
|
isEngineVersionMatched(versionExp: string): boolean {
|
|
const engineVersion = engineConfig.get('ENGINE_VERSION');
|
|
// ref: https://github.com/npm/node-semver#functions
|
|
// 1.0.1-beta should match '^1.0.0'
|
|
return semverSatisfies(engineVersion, versionExp, { includePrerelease: true });
|
|
}
|
|
|
|
/**
|
|
* register a plugin
|
|
* @param pluginConfigCreator - a creator function which returns the plugin config
|
|
* @param options - the plugin options
|
|
* @param registerOptions - the plugin register options
|
|
*/
|
|
async register(
|
|
pluginModel: IPublicTypePlugin,
|
|
options?: any,
|
|
registerOptions?: IPublicTypePluginRegisterOptions,
|
|
): Promise<void> {
|
|
// registerOptions maybe in the second place
|
|
if (isLowCodeRegisterOptions(options)) {
|
|
registerOptions = options;
|
|
options = {};
|
|
}
|
|
let { pluginName, meta = {} } = pluginModel;
|
|
const { preferenceDeclaration, engines } = meta;
|
|
// filter invalid eventPrefix
|
|
const { eventPrefix } = meta;
|
|
const isReservedPrefix = RESERVED_EVENT_PREFIX.find((item) => item === eventPrefix);
|
|
if (isReservedPrefix) {
|
|
meta.eventPrefix = undefined;
|
|
logger.warn(
|
|
`plugin ${pluginName} is trying to use ${eventPrefix} as event prefix, which is a reserved event prefix, please use another one`,
|
|
);
|
|
}
|
|
const ctx = this._getLowCodePluginContext({ pluginName, meta });
|
|
const customFilterValidOptions = engineConfig.get(
|
|
'customPluginFilterOptions',
|
|
filterValidOptions,
|
|
);
|
|
const pluginTransducer = engineConfig.get('customPluginTransducer', null);
|
|
const newPluginModel = pluginTransducer
|
|
? await pluginTransducer(pluginModel, ctx, options)
|
|
: pluginModel;
|
|
const newOptions = customFilterValidOptions(
|
|
options,
|
|
newPluginModel.meta?.preferenceDeclaration,
|
|
);
|
|
const config = newPluginModel(ctx, newOptions);
|
|
// compat the legacy way to declare pluginName
|
|
// @ts-ignore
|
|
pluginName = pluginName || config.name;
|
|
invariant(pluginName, 'pluginConfigCreator.pluginName required', config);
|
|
|
|
ctx.setPreference(pluginName, preferenceDeclaration!);
|
|
|
|
const allowOverride = registerOptions?.override === true;
|
|
|
|
if (this.pluginsMap.has(pluginName)) {
|
|
if (!allowOverride) {
|
|
throw new Error(`Plugin with name ${pluginName} exists`);
|
|
} else {
|
|
// clear existing plugin
|
|
const originalPlugin = this.pluginsMap.get(pluginName);
|
|
logger.log(
|
|
'plugin override, originalPlugin with name ',
|
|
pluginName,
|
|
' will be destroyed, config:',
|
|
originalPlugin?.config,
|
|
);
|
|
originalPlugin?.destroy();
|
|
this.pluginsMap.delete(pluginName);
|
|
}
|
|
}
|
|
|
|
const engineVersionExp = engines && engines.lowcodeEngine;
|
|
if (engineVersionExp && !this.isEngineVersionMatched(engineVersionExp)) {
|
|
throw new Error(
|
|
`plugin ${pluginName} skipped, engine check failed, current engine version is ${engineConfig.get('ENGINE_VERSION')}, meta.engines.lowcodeEngine is ${engineVersionExp}`,
|
|
);
|
|
}
|
|
|
|
const plugin = new LowCodePluginRuntime(pluginName, this, config, meta);
|
|
// support initialization of those plugins which registered
|
|
// after normal initialization by plugin-manager
|
|
if (registerOptions?.autoInit) {
|
|
await plugin.init();
|
|
}
|
|
this.plugins.push(plugin);
|
|
this.pluginsMap.set(pluginName, plugin);
|
|
logger.log(`plugin registered with pluginName: ${pluginName}, config: `, config, 'meta:', meta);
|
|
}
|
|
|
|
get(pluginName: string): ILowCodePluginRuntime | undefined {
|
|
return this.pluginsMap.get(pluginName);
|
|
}
|
|
|
|
getAll(): ILowCodePluginRuntime[] {
|
|
return this.plugins;
|
|
}
|
|
|
|
has(pluginName: string): boolean {
|
|
return this.pluginsMap.has(pluginName);
|
|
}
|
|
|
|
async delete(pluginName: string): Promise<boolean> {
|
|
const plugin = this.plugins.find(({ name }) => name === pluginName);
|
|
if (!plugin) return false;
|
|
await plugin.destroy();
|
|
const idx = this.plugins.indexOf(plugin);
|
|
this.plugins.splice(idx, 1);
|
|
return this.pluginsMap.delete(pluginName);
|
|
}
|
|
|
|
async init(pluginPreference?: PluginPreference) {
|
|
const pluginNames: string[] = [];
|
|
const pluginObj: { [name: string]: ILowCodePluginRuntime } = {};
|
|
this.pluginPreference = pluginPreference;
|
|
this.plugins.forEach((plugin) => {
|
|
pluginNames.push(plugin.name);
|
|
pluginObj[plugin.name] = plugin;
|
|
});
|
|
const { missingTasks, sequence } = sequencify(pluginObj, pluginNames);
|
|
invariant(!missingTasks.length, 'plugin dependency missing', missingTasks);
|
|
logger.log('load plugin sequence:', sequence);
|
|
|
|
for (const pluginName of sequence) {
|
|
try {
|
|
await this.pluginsMap.get(pluginName)!.init();
|
|
} catch (e) /* istanbul ignore next */ {
|
|
logger.error(
|
|
`Failed to init plugin:${pluginName}, it maybe affect those plugins which depend on this.`,
|
|
);
|
|
logger.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
async destroy() {
|
|
for (const plugin of this.plugins) {
|
|
await plugin.destroy();
|
|
}
|
|
}
|
|
|
|
get size() {
|
|
return this.pluginsMap.size;
|
|
}
|
|
|
|
getPluginPreference(
|
|
pluginName: string,
|
|
): Record<string, IPublicTypePreferenceValueType> | null | undefined {
|
|
if (!this.pluginPreference) {
|
|
return null;
|
|
}
|
|
return this.pluginPreference.get(pluginName);
|
|
}
|
|
|
|
toProxy() {
|
|
return new Proxy(this, {
|
|
get(target, prop, receiver) {
|
|
if (target.pluginsMap.has(prop as string)) {
|
|
// 禁用态的插件,直接返回 undefined
|
|
if (target.pluginsMap.get(prop as string)!.disabled) {
|
|
return undefined;
|
|
}
|
|
return target.pluginsMap.get(prop as string)?.toProxy();
|
|
}
|
|
return Reflect.get(target, prop, receiver);
|
|
},
|
|
});
|
|
}
|
|
|
|
/* istanbul ignore next */
|
|
setDisabled(pluginName: string, flag = true) {
|
|
logger.warn(`plugin:${pluginName} has been set disable:${flag}`);
|
|
this.pluginsMap.get(pluginName)?.setDisabled(flag);
|
|
}
|
|
|
|
async dispose() {
|
|
await this.destroy();
|
|
this.plugins = [];
|
|
this.pluginsMap.clear();
|
|
}
|
|
}
|