-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1a53aab
commit 52ad74b
Showing
13 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) ?? []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)!); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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][]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "@artus/test-plugin-c", | ||
"artusjsPlugin": { | ||
"name": "plugin-c" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`)); | ||
}); | ||
}); | ||
}); |