Skip to content

Commit

Permalink
feat(plugin): 插件基类实现与依赖链排序逻辑 (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
noahziheng authored Apr 11, 2022
1 parent 1a53aab commit 52ad74b
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
export * from './loader';
export * from './lifecycle';
export * from './exception';
export * from './plugin';
export * from './application';

import Trigger from './trigger';
Expand Down
36 changes: 36 additions & 0 deletions src/plugin/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Plugin, PluginConfigItem, PluginMetadata } from "./types";

export class BasePlugin implements Plugin {
public name: string;
public enable: boolean;
public importPath: string;
public metadata: Partial<PluginMetadata> = {};

constructor(name: string, configItem: PluginConfigItem) {
this.name = name;
const importPath = configItem.path ?? configItem.package;
if (!importPath) {
throw new Error(`Plugin ${name} need have path or package field`);
}
this.importPath = importPath;
this.enable = configItem.enable ?? false;
}

async init() {}

checkDepExisted(map: Map<string, BasePlugin>): void {
const depPluginNames = [
...(this.metadata.dependencies ?? []),
...(this.metadata.optionalDependencies ?? []),
];
for (const pluginName of depPluginNames) {
if (!map.has(pluginName)) {
throw new Error(`Plugin ${this.name} need have plugin ${pluginName} dependencies.`);
}
}
}

getDepEdgeList(): [string, string][] {
return this.metadata.dependencies?.map((depPluginName) => [this.name, depPluginName]) ?? [];
}
}
38 changes: 38 additions & 0 deletions src/plugin/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Plugin } from './types';

// A utils function that toplogical sort plugins
export function topologicalSort(pluginInstanceMap: Map<string, Plugin>, pluginDepEdgeList: [string, string][]): string[] {
const res: string[] = [];
const indegree: Map<string, number> = new Map();

pluginDepEdgeList.forEach(([to]) => {
indegree.set(to, (indegree.get(to) ?? 0) + 1);
});

const queue: string[] = [];

for (const [name] of pluginInstanceMap) {
if (!indegree.has(name)) {
queue.push(name);
}
}

while(queue.length) {
const cur = queue.shift()!;
res.push(cur);
for (const [to, from] of pluginDepEdgeList) {
if (from === cur) {
indegree.set(to, (indegree.get(to) ?? 0) - 1);
if (indegree.get(to) === 0) {
queue.push(to);
}
}
}
}

if (res.length !== pluginInstanceMap.size) {
const diffPlugin = [...pluginInstanceMap.keys()].filter((name) => !res.includes(name));
throw new Error(`There is a cycle in the dependencies, wrong plugin is ${diffPlugin.join(',')}.`);
}
return res;
}
28 changes: 28 additions & 0 deletions src/plugin/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BasePlugin } from './base';
import { topologicalSort } from './common';
import { ArtusPlugin } from './impl';
import { PluginConfigItem } from './types';

export class PluginFactory {
static async create(name: string, item: PluginConfigItem): Promise<BasePlugin> {
const pluginInstance = new ArtusPlugin(name, item);
await pluginInstance.init();
return pluginInstance;
}

static async createFromConfig(config: Record<string, PluginConfigItem>): Promise<BasePlugin[]> {
const pluginInstanceMap: Map<string, BasePlugin> = new Map();
for (const [name, item] of Object.entries(config)) {
const pluginInstance = await PluginFactory.create(name, item);
pluginInstanceMap.set(name, pluginInstance);
}
let pluginDepEdgeList: [string, string][] = [];
// Topological sort plugins
for (const [_name, pluginInstance] of pluginInstanceMap) {
pluginInstance.checkDepExisted(pluginInstanceMap);
pluginDepEdgeList = pluginDepEdgeList.concat(pluginInstance.getDepEdgeList());
}
const pluginSortResult: string[] = topologicalSort(pluginInstanceMap, pluginDepEdgeList);
return pluginSortResult.map((name) => pluginInstanceMap.get(name)!);
}
}
22 changes: 22 additions & 0 deletions src/plugin/impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BasePlugin } from './base';

export class ArtusPlugin extends BasePlugin {
async init() {
if (!this.enable) {
return;
}
let pkgJson: Record<string, any>;
try {
pkgJson = await import(this.importPath + '/package.json');
} catch (error) {
throw new Error(`${this.name} is not have a package.json file`);
}
if (!pkgJson?.artusjsPlugin) {
throw new Error(`${this.name} is not an Artus plugin`);
}
this.metadata = pkgJson.artusjsPlugin;
if (this.metadata.name !== this.name) {
throw new Error(`${this.name} metadata invalid, name is ${this.metadata.name}`);
}
}
}
5 changes: 5 additions & 0 deletions src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {} from './types';

export * from './base';
export * from './impl';
export * from './factory';
28 changes: 28 additions & 0 deletions src/plugin/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const enum PluginType {
simple = 'simple',
module = 'module',
}

export interface PluginMetadata {
name: string;
dependencies?: string[];
optionalDependencies?: string[];
type?: PluginType;
}

export interface PluginConfigItem {
enable: boolean;
path?: string;
package?: string;
}

export interface Plugin {
name: string;
enable: boolean;
importPath: string;
metadata: Partial<PluginMetadata>;

init(): Promise<void>;
checkDepExisted(map: Map<string, Plugin>): void;
getDepEdgeList(): [string, string][];
}
8 changes: 8 additions & 0 deletions test/fixtures/plugin-a/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@artus/test-plugin-a",
"artusjsPlugin": {
"name": "plugin-a",
"dependencies": [ "plugin-b" ],
"optionalDependencies": [ "plugin-c" ]
}
}
7 changes: 7 additions & 0 deletions test/fixtures/plugin-b/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@artus/test-plugin-b",
"artusjsPlugin": {
"name": "plugin-b",
"dependencies": [ "plugin-c" ]
}
}
6 changes: 6 additions & 0 deletions test/fixtures/plugin-c/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@artus/test-plugin-c",
"artusjsPlugin": {
"name": "plugin-c"
}
}
7 changes: 7 additions & 0 deletions test/fixtures/plugin-wrong-a/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@artus/test-plugin-wrong-a",
"artusjsPlugin": {
"name": "plugin-wrong-a",
"dependencies": [ "plugin-wrong-b" ]
}
}
7 changes: 7 additions & 0 deletions test/fixtures/plugin-wrong-b/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@artus/test-plugin-wrong-b",
"artusjsPlugin": {
"name": "plugin-wrong-b",
"dependencies": [ "plugin-wrong-a" ]
}
}
59 changes: 59 additions & 0 deletions test/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'reflect-metadata';
import path from 'path';
import { ArtusPlugin, PluginFactory } from '../src';

describe('test/app.test.ts', () => {
describe('app with config', () => {
it('should load plugin with dep order', async () => {
const mockPluginConfig = {
'plugin-a': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-a'),
},
'plugin-b': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-b'),
},
'plugin-c': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-c'),
}
}
const pluginList = await PluginFactory.createFromConfig(mockPluginConfig);
expect(pluginList.length).toEqual(3);
pluginList.forEach(plugin => {
expect(plugin).toBeInstanceOf(ArtusPlugin);
expect(plugin.enable).toBeTruthy();
});
expect(pluginList.map((plugin) => plugin.name)).toStrictEqual(['plugin-c', 'plugin-b', 'plugin-a']);
});

it('should not load plugin with wrong order', async () => {
const mockPluginConfig = {
'plugin-a': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-a'),
},
'plugin-b': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-b'),
},
'plugin-c': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-c'),
},
'plugin-wrong-a': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-wrong-a'),
},
'plugin-wrong-b': {
enable: true,
path: path.resolve(__dirname, './fixtures/plugin-wrong-b'),
}
}
expect(async () => {
await PluginFactory.createFromConfig(mockPluginConfig)
}).rejects.toThrowError(new Error(`There is a cycle in the dependencies, wrong plugin is plugin-wrong-a,plugin-wrong-b.`));
});
});
});

0 comments on commit 52ad74b

Please sign in to comment.