兼容打包成二进制

This commit is contained in:
COOL 2025-01-15 20:48:15 +08:00
parent fd8aaad60a
commit fcef095b2a
27 changed files with 1485 additions and 192 deletions

View File

@ -1,5 +1,5 @@
{
"name": "cool-admin-midway",
"name": "cool-admin",
"version": "8.0.0",
"description": "一个很酷的Ai快速开发框架",
"private": true,
@ -49,8 +49,7 @@
},
"scripts": {
"start": "NODE_ENV=production node ./bootstrap.js",
"entity": "cool entity",
"dev": "cool check entity && cross-env NODE_ENV=local mwtsc --cleanOutDir --watch --run @midwayjs/mock/app.js",
"dev": "rimraf src/index.ts && cool check entity && cross-env NODE_ENV=local mwtsc --cleanOutDir --watch --run @midwayjs/mock/app.js",
"test": "cross-env NODE_ENV=unittest jest",
"cov": "jest --coverage",
"lint": "mwts check",
@ -66,7 +65,9 @@
"pkg": {
"scripts": "dist/**/*",
"assets": [
"public/**/*"
"public/**/*",
"typings/**/*",
"cool/**/*"
],
"targets": [
"node18-macos-x64",

View File

@ -1,7 +1,7 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import * as moment from 'moment';
import { LocationUtil } from '@cool-midway/core';
import * as path from 'path';
/**
*
@ -12,8 +12,20 @@ export class Utils {
@Inject()
baseDir;
@Inject()
locationUtil: LocationUtil;
/**
* dist路径
*/
getDistPath() {
const runPath = __dirname;
const distIndex =
runPath.lastIndexOf('/dist/') !== -1
? runPath.lastIndexOf('/dist/')
: runPath.lastIndexOf('\\dist\\');
if (distIndex !== -1) {
return path.join(runPath.substring(0, distIndex), 'dist');
}
return path.join(runPath, 'dist');
}
/**
* IP

View File

@ -2,12 +2,14 @@ import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core';
import { CoolCacheStore } from '@cool-midway/core';
import * as path from 'path';
import { getUploadDir } from '../modules/plugin/hooks/upload';
// redis缓存
// import { redisStore } from 'cache-manager-ioredis-yet';
export default {
// use for cookie sign key, should change to your own and keep security
keys: '673dcd50-f95d-4109-b69d-aa80df64098e',
keys: '576848ea-bb0c-4c0c-ac95-c8602ef908b5',
koa: {
port: 8001,
},
@ -19,6 +21,10 @@ export default {
prefix: '/public',
dir: path.join(__dirname, '..', '..', 'public'),
},
static: {
prefix: '/upload',
dir: getUploadDir(),
},
},
},
// 文件上传

View File

@ -18,6 +18,7 @@ import * as LocalConfig from './config/config.local';
import * as ProdConfig from './config/config.prod';
import * as cool from '@cool-midway/core';
import * as upload from '@midwayjs/upload';
import * as os from 'os';
@Configuration({
imports: [
@ -60,15 +61,5 @@ export class MainConfiguration {
@Inject()
logger: ILogger;
async onReady() {
this.webRouterService.addRouter(
async ctx => {
ctx.redirect('/public/index.html');
},
{
url: '/',
requestMethod: 'GET',
}
);
}
async onReady() {}
}

View File

@ -1,14 +1,15 @@
// 自动生成的文件,请勿手动修改
import * as entity0 from './modules/base/entity/sys/user_role';
import * as entity1 from './modules/base/entity/sys/user';
import * as entity2 from './modules/base/entity/sys/role_menu';
import * as entity3 from './modules/base/entity/sys/role_department';
import * as entity4 from './modules/base/entity/sys/role';
import * as entity5 from './modules/base/entity/sys/param';
import * as entity6 from './modules/base/entity/sys/menu';
import * as entity7 from './modules/base/entity/sys/log';
import * as entity8 from './modules/base/entity/sys/department';
import * as entity9 from './modules/base/entity/sys/conf';
import * as entity0 from './modules/plugin/entity/info';
import * as entity1 from './modules/base/entity/sys/user_role';
import * as entity2 from './modules/base/entity/sys/user';
import * as entity3 from './modules/base/entity/sys/role_menu';
import * as entity4 from './modules/base/entity/sys/role_department';
import * as entity5 from './modules/base/entity/sys/role';
import * as entity6 from './modules/base/entity/sys/param';
import * as entity7 from './modules/base/entity/sys/menu';
import * as entity8 from './modules/base/entity/sys/log';
import * as entity9 from './modules/base/entity/sys/department';
import * as entity10 from './modules/base/entity/sys/conf';
export const entities = [
...Object.values(entity0),
...Object.values(entity1),
@ -20,4 +21,5 @@ export const entities = [
...Object.values(entity7),
...Object.values(entity8),
...Object.values(entity9),
...Object.values(entity10),
];

View File

@ -3,6 +3,7 @@
export * from './comm/utils';
export * from './config/config.default';
export * from './config/config.local';
export * from './modules/plugin/entity/info';
export * from './modules/base/entity/sys/user_role';
export * from './modules/base/entity/sys/user';
export * from './modules/base/entity/sys/role_menu';
@ -21,6 +22,11 @@ export * from './modules/base/service/sys/log';
export * from './modules/base/middleware/log';
export * from './modules/base/middleware/authority';
export * from './modules/base/config';
export * from './modules/plugin/interface';
export * from './modules/plugin/service/center';
export * from './modules/plugin/event/init';
export * from './modules/plugin/service/types';
export * from './modules/plugin/service/info';
export * from './modules/base/dto/login';
export * from './modules/base/service/sys/data';
export * from './modules/base/service/sys/menu';
@ -39,6 +45,11 @@ export * from './modules/base/controller/admin/sys/param';
export * from './modules/base/controller/admin/sys/role';
export * from './modules/base/controller/admin/sys/user';
export * from './modules/base/controller/app/comm';
export * from './modules/base/event/app';
export * from './modules/base/event/menu';
export * from './modules/base/job/log';
export * from './modules/plugin/config';
export * from './modules/plugin/controller/admin/info';
export * from './modules/plugin/event/app';
export * from './modules/plugin/hooks/base';
export * from './modules/plugin/hooks/upload/interface';
export * from './modules/plugin/hooks/upload/index';

View File

@ -7,7 +7,7 @@ import {
} from '@cool-midway/core';
import { ALL, Body, Get, Inject, Post, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
// import { PluginService } from '../../../plugin/service/info';
import { PluginService } from '../../../plugin/service/info';
import { BaseSysUserEntity } from '../../entity/sys/user';
import { BaseSysLoginService } from '../../service/sys/login';
import { BaseSysPermsService } from '../../service/sys/perms';
@ -32,8 +32,8 @@ export class BaseCommController extends BaseController {
@Inject()
ctx: Context;
// @Inject()
// pluginService: PluginService;
@Inject()
pluginService: PluginService;
/**
*
@ -69,8 +69,8 @@ export class BaseCommController extends BaseController {
*/
@Post('/upload', { summary: '文件上传' })
async upload() {
// const file = await this.pluginService.getInstance('upload');
// return this.ok(await file.upload(this.ctx));
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.upload(this.ctx));
}
/**
@ -78,8 +78,8 @@ export class BaseCommController extends BaseController {
*/
@Get('/uploadMode', { summary: '文件上传模式' })
async uploadMode() {
// const file = await this.pluginService.getInstance('upload');
// return this.ok(await file.getMode());
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.getMode());
}
/**

View File

@ -9,7 +9,7 @@ import {
} from '@cool-midway/core';
import { Context } from '@midwayjs/koa';
import { BaseSysParamService } from '../../service/sys/param';
// import { PluginService } from '../../../plugin/service/info';
import { PluginService } from '../../../plugin/service/info';
/**
*
@ -18,8 +18,8 @@ import { BaseSysParamService } from '../../service/sys/param';
@Provide()
@CoolController()
export class BaseAppCommController extends BaseController {
// @Inject()
// pluginService: PluginService;
@Inject()
pluginService: PluginService;
@Inject()
ctx: Context;
@ -57,8 +57,8 @@ export class BaseAppCommController extends BaseController {
*/
@Post('/upload', { summary: '文件上传' })
async upload() {
// const file = await this.pluginService.getInstance('upload');
// return this.ok(await file.upload(this.ctx));
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.upload(this.ctx));
}
/**
@ -66,7 +66,7 @@ export class BaseAppCommController extends BaseController {
*/
@Get('/uploadMode', { summary: '文件上传模式' })
async uploadMode() {
// const file = await this.pluginService.getInstance('upload');
// return this.ok(await file.getMode());
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.getMode());
}
}

View File

@ -1,102 +0,0 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { App, Config, ILogger, Logger } from '@midwayjs/core';
import { IMidwayKoaApplication } from '@midwayjs/koa';
import * as fs from 'fs';
import * as path from 'path';
import { v1 as uuid } from 'uuid';
/**
* jwt.secret
*/
@CoolEvent()
export class BaseAppEvent {
@Logger()
coreLogger: ILogger;
@Config('module')
config;
@Config('keys')
configKeys;
@Config('koa.port')
port;
@App()
app: IMidwayKoaApplication;
@Event('onMenuInit')
async onMenuInit() {
if (this.app.getEnv() != 'local') return;
this.checkConfig();
this.checkKeys();
}
@Event('onServerReady')
async onServerReady() {
this.coreLogger.info(`服务启动成功,端口:${this.port}`);
}
/**
*
*/
async checkConfig() {
if (this.config.base.jwt.secret == 'cool-admin-xxxxxx') {
this.coreLogger.warn(
'\x1B[36m 检测到模块[base] jwt.secret 配置是默认值,请不要关闭!即将自动修改... \x1B[0m'
);
setTimeout(() => {
const filePath = path.join(
this.app.getBaseDir(),
'..',
'src',
'modules',
'base',
'config.ts'
);
// 替换文件内容
let fileData = fs.readFileSync(filePath, 'utf8');
const secret = uuid().replace(/-/g, '');
this.config.base.jwt.secret = secret;
fs.writeFileSync(
filePath,
fileData.replace('cool-admin-xxxxxx', secret)
);
this.coreLogger.info(
'\x1B[36m [cool:module:base] midwayjs cool module base auto modify jwt.secret\x1B[0m'
);
}, 6000);
}
}
/**
* keys
*/
async checkKeys() {
if (this.configKeys == 'cool-admin-keys-xxxxxx') {
this.coreLogger.warn(
'\x1B[36m 检测到基础配置[Keys] 是默认值,请不要关闭!即将自动修改... \x1B[0m'
);
setTimeout(() => {
const filePath = path.join(
this.app.getBaseDir(),
'..',
'src',
'config',
'config.default.ts'
);
// 替换文件内容
let fileData = fs.readFileSync(filePath, 'utf8');
const secret = uuid().replace(/-/g, '');
this.config.base.jwt.secret = secret;
fs.writeFileSync(
filePath,
fileData.replace('cool-admin-keys-xxxxxx', secret)
);
this.coreLogger.info(
'\x1B[36m [cool:module:base] midwayjs cool keys auto modify \x1B[0m'
);
}, 6000);
}
}
}

View File

@ -0,0 +1,27 @@
import { ModuleConfig } from '@cool-midway/core';
/**
*
*/
export default options => {
return {
// 模块名称
name: '插件模块',
// 模块描述
description: '插件查看、安装、卸载、配置等',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
// 基础插件配置
hooks: {
// 文件上传
upload: {
// 地址前缀
domain: `http://127.0.0.1:${options?.app?.getConfig('koa.port')}`,
},
},
} as ModuleConfig;
};

View File

@ -0,0 +1,56 @@
import {
CoolController,
BaseController,
CoolTag,
CoolUrlTag,
TagTypes,
} from '@cool-midway/core';
import { PluginInfoEntity } from '../../entity/info';
import { Body, Fields, Files, Inject, Post } from '@midwayjs/core';
import { PluginService } from '../../service/info';
/**
*
*/
@CoolUrlTag({
key: TagTypes.IGNORE_TOKEN,
value: [],
})
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: PluginInfoEntity,
service: PluginService,
pageQueryOp: {
select: [
'a.id',
'a.name',
'a.keyName',
'a.hook',
'a.version',
'a.status',
'a.readme',
'a.author',
'a.logo',
'a.description',
'a.pluginJson',
'a.config',
'a.createTime',
'a.updateTime',
],
addOrderBy: {
id: 'DESC',
},
},
})
export class AdminPluginInfoController extends BaseController {
@Inject()
pluginService: PluginService;
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/install', { summary: '安装插件' })
async install(@Files() files, @Fields() fields) {
return this.ok(
await this.pluginService.install(files[0].data, fields.force)
);
}
}

View File

@ -0,0 +1,44 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity, DataSource, Index } from 'typeorm';
console.log(DataSource);
/**
*
*/
@Entity('plugin_info')
export class PluginInfoEntity extends BaseEntity {
@Column({ comment: '名称' })
name: string;
@Column({ comment: '简介' })
description: string;
@Index()
@Column({ comment: 'Key名' })
keyName: string;
@Column({ comment: 'Hook' })
hook: string;
@Column({ comment: '描述', type: 'text' })
readme: string;
@Column({ comment: '版本' })
version: string;
@Column({ comment: 'Logo(base64)', type: 'text', nullable: true })
logo: string;
@Column({ comment: '作者' })
author: string;
@Column({ comment: '状态 0-禁用 1-启用', default: 0 })
status: number;
@Column({ comment: '插件的plugin.json', type: 'json', nullable: true })
pluginJson: any;
@Column({ comment: '配置', type: 'json', nullable: true })
config: any;
}

View File

@ -0,0 +1,44 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
import {
App,
Config,
ILogger,
Inject,
InjectClient,
Logger,
} from '@midwayjs/core';
import { IMidwayKoaApplication } from '@midwayjs/koa';
import { PLUGIN_CACHE_KEY, PluginCenterService } from '../service/center';
import { PluginTypesService } from '../service/types';
/**
*
*/
@CoolEvent()
export class PluginAppEvent {
@Logger()
coreLogger: ILogger;
@Config('module')
config;
@App()
app: IMidwayKoaApplication;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
pluginCenterService: PluginCenterService;
@Inject()
pluginTypesService: PluginTypesService;
@Event('onServerReady')
async onServerReady() {
await this.midwayCache.set(PLUGIN_CACHE_KEY, []);
this.pluginCenterService.init();
// this.pluginTypesService.reGenerate();
}
}

View File

@ -0,0 +1,36 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { Inject } from '@midwayjs/core';
import { PluginCenterService } from '../service/center';
// 插件初始化全局事件
export const GLOBAL_EVENT_PLUGIN_INIT = 'globalPluginInit';
// 插件移除全局事件
export const GLOBAL_EVENT_PLUGIN_REMOVE = 'globalPluginRemove';
/**
*
*/
@CoolEvent()
export class PluginInitEvent {
@Inject()
pluginCenterService: PluginCenterService;
/**
*
* @param key
*/
@Event(GLOBAL_EVENT_PLUGIN_INIT)
async globalPluginInit(key: string) {
await this.pluginCenterService.initOne(key);
}
/**
*
* @param key
* @param isHook
*/
@Event(GLOBAL_EVENT_PLUGIN_REMOVE)
async globalPluginRemove(key: string, isHook: boolean) {
await this.pluginCenterService.remove(key, isHook);
}
}

View File

@ -0,0 +1,26 @@
import { IMidwayContext, IMidwayApplication } from '@midwayjs/core';
import { PluginInfo } from '../interface';
/**
* hook基类
*/
export class BasePluginHook {
/** 请求上下文用到此项无法本地调试需安装到cool-admin中才能调试 */
ctx: IMidwayContext;
/** 应用实例用到此项无法本地调试需安装到cool-admin中才能调试 */
app: IMidwayApplication;
/** 插件信息 */
pluginInfo: PluginInfo;
/**
*
*/
async init(
pluginInfo: PluginInfo,
ctx?: IMidwayContext,
app?: IMidwayApplication
) {
this.pluginInfo = pluginInfo;
this.ctx = ctx;
this.app = app;
}
}

View File

@ -0,0 +1,120 @@
import { BaseUpload, MODETYPE } from './interface';
import { BasePluginHook } from '../base';
import * as fs from 'fs';
import * as path from 'path';
import * as moment from 'moment';
import { v1 as uuid } from 'uuid';
import { CoolCommException } from '@cool-midway/core';
import * as _ from 'lodash';
import * as os from 'os';
import * as pkg from '../../../../../package.json';
// 获得上传目录
export const getUploadDir = () => {
const uploadDir = path.join(os.homedir(), `.${pkg.name}`, 'upload');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
return uploadDir;
};
/**
*
*/
export class CoolPlugin extends BasePluginHook implements BaseUpload {
/**
*
* @returns
*/
async getMode() {
return {
mode: MODETYPE.LOCAL,
type: MODETYPE.LOCAL,
};
}
/**
*
* @returns
*/
async getMetaFileObj() {
return;
}
/**
*
* @param url
* @param fileName
*/
async downAndUpload(url: string, fileName?: string) {
const { domain } = this.pluginInfo.config;
// 从url获取扩展名
const extend = path.extname(url);
const download = require('download');
// 数据
const data = url.includes('http')
? await download(url)
: fs.readFileSync(url);
// 创建文件夹
const dirPath = path.join(getUploadDir(), `${moment().format('YYYYMMDD')}`);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
const uuidStr = uuid();
const name = `${moment().format('YYYYMMDD')}/${
fileName ? fileName : uuidStr + extend
}`;
fs.writeFileSync(
`${dirPath}/${fileName ? fileName : uuid() + extend}`,
data
);
return `${domain}/upload/${name}`;
}
/**
* Key()
* @param filePath
* @param key
*/
async uploadWithKey(filePath: any, key: any) {
const { domain } = this.pluginInfo.config;
const data = fs.readFileSync(filePath);
fs.writeFileSync(path.join(this.app.getBaseDir(), '..', key), data);
return domain + key;
}
/**
*
* @param ctx
* @param key
*/
async upload(ctx: any) {
const { domain } = this.pluginInfo.config;
try {
const { key } = ctx.fields;
if (_.isEmpty(ctx.files)) {
throw new CoolCommException('上传文件为空');
}
const basePath = getUploadDir();
const file = ctx.files[0];
const extension = file.filename.split('.').pop();
const name =
moment().format('YYYYMMDD') + '/' + (key || `${uuid()}.${extension}`);
const target = path.join(basePath, name);
const dirPath = path.join(basePath, moment().format('YYYYMMDD'));
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const data = fs.readFileSync(file.data);
fs.writeFileSync(target, data);
return domain + '/upload/' + name;
} catch (err) {
console.error(err);
throw new CoolCommException('上传失败' + err.message);
}
}
}
// 导出插件实例, Plugin名称不可修改
export const Plugin = CoolPlugin;

View File

@ -0,0 +1,56 @@
// 模式
export enum MODETYPE {
// 本地
LOCAL = 'local',
// 云存储
CLOUD = 'cloud',
// 其他
OTHER = 'other',
}
/**
*
*/
export interface Mode {
// 模式
mode: MODETYPE;
// 类型
type: string;
}
/**
*
*/
export interface BaseUpload {
/**
*
*/
getMode(): Promise<Mode>;
/**
*
* @returns
*/
getMetaFileObj(): Promise<any>;
/**
*
* @param url
* @param fileName
*/
downAndUpload(url: string, fileName?: string): Promise<string>;
/**
* Key()
* @param filePath
* @param key
*/
uploadWithKey(filePath, key): Promise<string>;
/**
*
* @param ctx
* @param key
*/
upload(ctx): Promise<string>;
}

View File

@ -0,0 +1,25 @@
/**
*
*/
export interface PluginInfo {
/** 名称 */
name?: string;
/** 唯一标识 */
key?: string;
/** 钩子 */
hook?: string;
/** 是否单例 */
singleton?: boolean;
/** 版本 */
version?: string;
/** 描述 */
description?: string;
/** 作者 */
author?: string;
/** logo */
logo?: string;
/** README 使用说明 */
readme?: string;
/** 配置 */
config?: any;
}

View File

@ -0,0 +1,200 @@
import {
App,
IMidwayApplication,
Inject,
InjectClient,
Scope,
Provide,
ScopeEnum,
} from '@midwayjs/core';
import * as fs from 'fs';
import * as path from 'path';
import { PluginInfoEntity } from '../entity/info';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { PluginInfo } from '../interface';
import * as _ from 'lodash';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
import { CoolEventManager } from '@cool-midway/core';
import { PluginService } from './info';
export const PLUGIN_CACHE_KEY = 'plugin:init';
export const EVENT_PLUGIN_READY = 'EVENT_PLUGIN_READY';
/**
*
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class PluginCenterService {
// 插件列表
plugins: Map<string, any> = new Map();
// 插件配置
pluginInfos: Map<string, PluginInfo> = new Map();
@App()
app: IMidwayApplication;
@InjectEntityModel(PluginInfoEntity)
pluginInfoEntity: Repository<PluginInfoEntity>;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
coolEventManager: CoolEventManager;
@Inject()
pluginService: PluginService;
/**
*
* @returns
*/
async init() {
this.plugins.clear();
await this.initHooks();
await this.initPlugin();
this.coolEventManager.emit(EVENT_PLUGIN_READY);
}
/**
*
* @param keyName key名
*/
async initOne(keyName: string) {
await this.initPlugin({
keyName,
});
this.coolEventManager.emit(EVENT_PLUGIN_READY, keyName);
}
/**
*
* @param keyName
* @param isHook
*/
async remove(keyName: string, isHook = false) {
this.plugins.delete(keyName);
this.pluginInfos.delete(keyName);
if (isHook) {
await this.initHooks();
}
}
/**
*
* @param key
* @param cls
* @param pluginInfo
*/
async register(key: string, cls: any, pluginInfo?: PluginInfo) {
// 单例插件
if (pluginInfo?.singleton) {
const instance = new cls();
await instance.init(this.pluginInfos.get(key), null, this.app, {
cache: this.midwayCache,
pluginService: this.pluginService,
});
this.plugins.set(key, instance);
} else {
// 普通插件
this.plugins.set(key, cls);
}
}
/**
*
*/
async initHooks() {
const hooksPath = path.join(
this.app.getBaseDir(),
'modules',
'plugin',
'hooks'
);
for (const key of fs.readdirSync(hooksPath)) {
const stat = fs.statSync(path.join(hooksPath, key));
if (!stat.isDirectory()) {
continue;
}
const { Plugin } = await import(path.join(hooksPath, key, 'index'));
await this.register(key, Plugin);
this.pluginInfos.set(key, {
name: key,
config: this.app.getConfig('module.plugin.hooks.' + key),
});
}
}
/**
*
* @param condition
*/
async initPlugin(condition?: {
hook?: string;
id?: number;
keyName?: string;
}) {
let find: any = { status: 1 };
if (condition) {
find = {
...find,
...condition,
};
}
const plugins = await this.pluginInfoEntity.findBy(find);
for (const plugin of plugins) {
const data = await this.pluginService.getData(plugin.keyName);
if (!data) {
continue;
}
const instance = await this.getInstance(data.content.data);
const pluginInfo = {
...plugin.pluginJson,
config: this.getConfig(plugin.config),
};
if (plugin.hook) {
this.pluginInfos.set(plugin.hook, pluginInfo);
await this.register(plugin.hook, instance, pluginInfo);
} else {
this.pluginInfos.set(plugin.keyName, pluginInfo);
await this.register(plugin.keyName, instance, pluginInfo);
}
}
}
/**
*
* @param config
* @returns
*/
private getConfig(config: any) {
const env = this.app.getEnv();
let isMulti = false;
for (const key in config) {
if (key.includes('@')) {
isMulti = true;
break;
}
}
return isMulti ? config[`@${env}`] : config;
}
/**
*
* @param content
* @returns
*/
async getInstance(content: string) {
let _instance;
const script = `
${content}
_instance = Plugin;
`;
eval(script);
return _instance;
}
}

View File

@ -0,0 +1,447 @@
import {
BaseService,
CoolCommException,
CoolEventManager,
} from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Equal, In, Not, Repository } from 'typeorm';
import { PluginInfoEntity } from '../entity/info';
import {
App,
Config,
ILogger,
IMidwayApplication,
IMidwayContext,
Inject,
InjectClient,
Logger,
Provide,
} from '@midwayjs/core';
import * as _ from 'lodash';
import { PluginInfo } from '../interface';
import { PluginCenterService } from './center';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
import {
GLOBAL_EVENT_PLUGIN_INIT,
GLOBAL_EVENT_PLUGIN_REMOVE,
} from '../event/init';
import { PluginMap, AnyString } from '../../../../typings/plugin';
import { PluginTypesService } from './types';
import * as path from 'path';
import * as fs from 'fs';
/**
*
*/
@Provide()
export class PluginService extends BaseService {
@InjectEntityModel(PluginInfoEntity)
pluginInfoEntity: Repository<PluginInfoEntity>;
@Inject()
ctx: IMidwayContext;
@App()
app: IMidwayApplication;
@Inject()
pluginCenterService: PluginCenterService;
@Config('module.plugin.hooks')
hooksConfig;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
coolEventManager: CoolEventManager;
@Inject()
pluginTypesService: PluginTypesService;
@Logger()
logger: ILogger;
/**
*
* @param param
* @param type
*/
async addOrUpdate(param: any, type?: 'add' | 'update') {
await super.addOrUpdate(param, type);
const info = await this.pluginInfoEntity
.createQueryBuilder('a')
.select(['a.id', 'a.keyName', 'a.status', 'a.hook'])
.where({
id: Equal(param.id),
})
.getOne();
if (info.status == 1) {
await this.reInit(info.keyName);
} else {
await this.remove(info.keyName, !!info.hook);
}
}
/**
*
*/
async reInit(keyName: string) {
// 多进程发送全局事件pm2下生效本地开发则通过普通事件
this.coolEventManager.globalEmit(GLOBAL_EVENT_PLUGIN_INIT, false, keyName);
}
/**
*
* @param keyName
* @param isHook
*/
async remove(keyName: string, isHook = false) {
// 多进程发送全局事件pm2下生效
this.coolEventManager.globalEmit(
GLOBAL_EVENT_PLUGIN_REMOVE,
false,
keyName,
isHook
);
this.pluginTypesService.deleteDtsFile(keyName);
}
/**
*
* @param ids
*/
async delete(ids: any) {
const list = await this.pluginInfoEntity.findBy({ id: In(ids) });
for (const item of list) {
await this.remove(item.keyName, !!item.hook);
// 删除文件
await this.deleteData(item.keyName);
}
await this.pluginInfoEntity.delete(ids);
}
/**
*
* @param param
*/
async update(param: any) {
const old = await this.pluginInfoEntity.findOne({
where: { id: param.id },
select: ['id', 'status', 'hook'],
});
// 启用插件,禁用同名插件
if (old.hook && param.status == 1 && old.status != param.status) {
await this.pluginInfoEntity.update(
{ hook: old.hook, status: 1, id: Not(old.id) },
{ status: 0 }
);
}
await super.update(param);
}
/**
*
* @param key
*/
async getConfig(key: string) {
return this.pluginCenterService.pluginInfos.get(key)?.config;
}
/**
*
* @param key key
* @param method
* @param params
* @returns
*/
async invoke<K extends keyof PluginMap>(
key: K | AnyString,
method: string,
...params
) {
// 实例
const instance: any = await this.getInstance(key);
return await instance[method](...params);
}
/**
*
* @param key
* @returns
*/
async getInstance<K extends keyof PluginMap>(
key: K | AnyString
): Promise<K extends keyof PluginMap ? PluginMap[K] : any> {
const check = await this.checkStatus(key);
if (!check) throw new CoolCommException(`插件[${key}]不存在或已禁用`);
let instance;
const pluginInfo = this.pluginCenterService.pluginInfos.get(key);
if (pluginInfo.singleton) {
instance = this.pluginCenterService.plugins.get(key);
} else {
instance = new (await this.pluginCenterService.plugins.get(key))();
await instance.init(pluginInfo, this.ctx, this.app, {
cache: this.midwayCache,
pluginService: this,
});
}
return instance;
}
/**
*
* @param key
*/
async checkStatus(key: string) {
if (Object.keys(this.hooksConfig).includes(key)) {
return true;
}
const info = await this.pluginInfoEntity
.createQueryBuilder('a')
.select(['a.id', 'a.status'])
.where({ status: 1, keyName: Equal(key) })
.getOne();
return !!info;
}
/**
*
* @param filePath
*/
async check(filePath: string) {
let data;
try {
data = await this.data(filePath);
} catch (e) {
return {
type: 0,
message: `插件信息不完整,请检查${data.errorData}`,
};
}
const check = await this.pluginInfoEntity.findOne({
where: { keyName: Equal(data.pluginJson.key) },
select: ['id', 'hook', 'status'],
});
if (check && !check.hook) {
return {
type: 1,
message: '插件已存在,继续安装将覆盖',
};
}
if (check && check.hook && check.status == 1) {
return {
type: 2,
message:
'已存在同名Hook插件你可以继续安装但是多个相同的Hook插件只能同时开启一个',
};
}
return {
type: 3,
message: '检查通过',
};
}
/**
*
* @param filePath
*/
async data(filePath: string): Promise<{
pluginJson: any;
readme: string;
logo: string;
content: string;
tsContent: string;
errorData: string;
}> {
const decompress = require('decompress');
const files = await decompress(filePath);
let errorData;
let pluginJson: PluginInfo,
readme: string,
logo: string,
content: string,
tsContent: string;
try {
errorData = 'plugin.json';
pluginJson = JSON.parse(
_.find(files, { path: 'plugin.json', type: 'file' }).data.toString()
);
errorData = 'readme';
readme = _.find(files, {
path: pluginJson.readme,
type: 'file',
}).data.toString();
errorData = 'logo';
logo = _.find(files, {
path: pluginJson.logo,
type: 'file',
}).data.toString('base64');
content = _.find(files, {
path: 'src/index.js',
type: 'file',
}).data.toString();
tsContent =
_.find(files, {
path: 'source/index.ts',
type: 'file',
})?.data?.toString() || '';
} catch (e) {
throw new CoolCommException('插件信息不完整');
}
return {
pluginJson,
readme,
logo,
content,
tsContent,
errorData,
};
}
/**
*
* @param file
* @param force
*/
async install(filePath: string, force = false) {
const forceBool = typeof force === 'string' ? force === 'true' : force;
const checkResult = await this.check(filePath);
if (checkResult.type != 3 && !forceBool) {
return checkResult;
}
const { pluginJson, readme, logo, content, tsContent } = await this.data(
filePath
);
if (pluginJson.key == 'plugin') {
throw new CoolCommException('插件key不能为plugin请更换其他key');
}
const check = await this.pluginInfoEntity.findOne({
where: { keyName: Equal(pluginJson.key) },
select: ['id', 'status', 'config'],
});
const data = {
name: pluginJson.name,
keyName: pluginJson.key,
version: pluginJson.version,
author: pluginJson.author,
hook: pluginJson.hook,
readme,
logo,
description: pluginJson.description,
pluginJson,
config: pluginJson.config,
status: 1,
} as PluginInfoEntity;
// 存在同名插件,更新,保留配置
if (check) {
await this.pluginInfoEntity.update(check.id, {
...data,
status: check.status,
config: {
...pluginJson.config,
...check.config,
},
});
} else {
// 全新安装
await this.pluginInfoEntity.insert(data);
}
// 保存插件内容
await this.saveData(
{
content: {
type: 'comm',
data: content,
},
tsContent: {
type: 'ts',
data: tsContent,
},
},
pluginJson.key
);
this.pluginTypesService.generateDtsFile(pluginJson.key, tsContent);
// 初始化插件
await this.reInit(pluginJson.key);
}
/**
*
* @param content
* @param keyName key
*/
async saveData(
data: {
content: {
type: 'comm' | 'module';
data: string;
};
tsContent: {
type: 'ts';
data: string;
};
},
keyName: string
) {
const filePath = this.pluginPath(keyName);
// 确保目录存在
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 写入文件,如果存在则覆盖
fs.writeFileSync(filePath, JSON.stringify(data, null, 0), { flag: 'w' });
}
/**
*
* @param keyName
* @returns
*/
async getData(keyName: string): Promise<{
content: {
type: 'comm' | 'module';
data: string;
};
tsContent: {
type: 'ts';
data: string;
};
}> {
const filePath = this.pluginPath(keyName);
if (!fs.existsSync(filePath)) {
this.logger.warn(
`插件[${keyName}]文件不存在,请卸载后重新安装: ${filePath}`
);
return null;
}
return JSON.parse(await fs.promises.readFile(filePath, 'utf-8'));
}
/**
*
* @param keyName
*/
async deleteData(keyName: string) {
const filePath = this.pluginPath(keyName);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
/**
*
* @param keyName
* @returns
*/
pluginPath(keyName: string) {
return path.join(
this.app.getBaseDir(),
'..',
'cool',
'plugin',
`${keyName}.cool`
);
}
}

View File

@ -0,0 +1,256 @@
import { BaseService } from '@cool-midway/core';
import { App, IMidwayApplication, Inject, Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import * as fs from 'fs';
import * as path from 'path';
import * as prettier from 'prettier';
import { Repository } from 'typeorm';
import * as ts from 'typescript';
import { Utils } from '../../../comm/utils';
import { PluginInfoEntity } from '../entity/info';
import { PluginService } from './info';
/**
*
*/
@Provide()
export class PluginTypesService extends BaseService {
@App()
app: IMidwayApplication;
@InjectEntityModel(PluginInfoEntity)
pluginInfoEntity: Repository<PluginInfoEntity>;
@Inject()
pluginService: PluginService;
@Inject()
utils: Utils;
/**
* d.ts文件
* @param tsContent
* @returns
*/
async dtsContent(tsContent: string) {
let output = '';
const compilerHost: ts.CompilerHost = {
fileExists: ts.sys.fileExists,
getCanonicalFileName: ts.sys.useCaseSensitiveFileNames
? s => s
: s => s.toLowerCase(),
getCurrentDirectory: ts.sys.getCurrentDirectory,
getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
getDirectories: ts.sys.getDirectories,
getNewLine: () => ts.sys.newLine,
getSourceFile: (fileName, languageVersion) => {
if (fileName === 'file.ts') {
return ts.createSourceFile(
fileName,
tsContent,
languageVersion,
true
);
}
const filePath = ts.sys.resolvePath(fileName);
return ts.sys.readFile(filePath)
? ts.createSourceFile(
filePath,
ts.sys.readFile(filePath)!,
languageVersion,
true
)
: undefined;
},
readFile: ts.sys.readFile,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
writeFile: (fileName, content) => {
if (fileName.includes('file.d.ts')) {
output = content || output;
}
},
};
const options: ts.CompilerOptions = {
declaration: true,
emitDeclarationOnly: true,
outDir: './',
skipLibCheck: true,
skipDefaultLibCheck: true,
noEmitOnError: false,
target: ts.ScriptTarget.ES2018,
strict: false,
module: ts.ModuleKind.Node16,
moduleResolution: ts.ModuleResolutionKind.Node16,
types: ['node'],
};
const program = ts.createProgram(['file.ts'], options, compilerHost);
program.emit();
if (!output) {
// Provide a default value if the output is still empty
output = '/* No declaration content generated */';
}
return output;
}
/**
* d.ts文件
* @param key
* @param tsContent
* @returns
*/
async generateDtsFile(key: string, tsContent: string) {
const env = this.app.getEnv();
// 不是本地开发环境不生成d.ts文件
if (env != 'local' || !tsContent) {
return;
}
// 基础路径
const basePath = path.join(this.app.getBaseDir(), '..', 'typings');
// pluginDts文件路径
const pluginDtsPath = path.join(basePath, 'plugin.d.ts');
// plugin文件夹路径
const pluginPath = path.join(basePath, `${key}.d.ts`);
// 生成d.ts文件
const dtsContent = await this.dtsContent(tsContent);
// 读取plugin.d.ts文件内容
let pluginDtsContent = fs.readFileSync(pluginDtsPath, 'utf-8');
// 根据key判断是否在PluginMap中存在
const keyWithHyphen = key.includes('-');
const importStatement = keyWithHyphen
? `import * as ${key.replace(/-/g, '_')} from './${key}';`
: `import * as ${key} from './${key}';`;
const pluginMapEntry = keyWithHyphen
? `'${key}': ${key.replace(/-/g, '_')}.CoolPlugin;`
: `${key}: ${key}.CoolPlugin;`;
// 检查import语句是否已经存在若不存在则添加
if (!pluginDtsContent.includes(importStatement)) {
pluginDtsContent = `${importStatement}\n${pluginDtsContent}`;
}
// 检查PluginMap中的键是否存在若不存在则添加
if (pluginDtsContent.includes(pluginMapEntry)) {
// 键存在则覆盖
const regex = new RegExp(
`(\\s*${keyWithHyphen ? `'${key}'` : key}:\\s*[^;]+;)`
);
pluginDtsContent = pluginDtsContent.replace(regex, pluginMapEntry);
} else {
// 键不存在则追加
const pluginMapRegex = /interface\s+PluginMap\s*{([^}]*)}/;
pluginDtsContent = pluginDtsContent.replace(
pluginMapRegex,
(match, p1) => {
return match.replace(p1, `${p1.trim()}\n ${pluginMapEntry}`);
}
);
}
// 格式化内容
pluginDtsContent = await this.formatContent(pluginDtsContent);
// 延迟2秒写入文件
setTimeout(async () => {
// 写入d.ts文件如果存在则覆盖
fs.writeFile(pluginPath, await this.formatContent(dtsContent), () => {});
// 写入plugin.d.ts文件
fs.writeFile(pluginDtsPath, pluginDtsContent, () => {});
}, 2000);
}
/**
* d.ts文件中的指定key
* @param key
*/
async deleteDtsFile(key: string) {
const env = this.app.getEnv();
// 不是本地开发环境不删除d.ts文件
if (env != 'local') {
return;
}
// 基础路径
const basePath = path.join(this.app.getBaseDir(), '..', 'typings');
// pluginDts文件路径
const pluginDtsPath = path.join(basePath, 'plugin.d.ts');
// plugin文件夹路径
const pluginPath = path.join(basePath, `${key}.d.ts`);
// 读取plugin.d.ts文件内容
let pluginDtsContent = fs.readFileSync(pluginDtsPath, 'utf-8');
// 根据key判断是否在PluginMap中存在
const keyWithHyphen = key.includes('-');
const importStatement = keyWithHyphen
? `import \\* as ${key.replace(/-/g, '_')} from '\\./${key}';`
: `import \\* as ${key} from '\\./${key}';`;
const pluginMapEntry = keyWithHyphen
? `'${key}': ${key.replace(/-/g, '_')}.CoolPlugin;`
: `${key}: ${key}.CoolPlugin;`;
// 删除import语句
const importRegex = new RegExp(`${importStatement}\\n`, 'g');
pluginDtsContent = pluginDtsContent.replace(importRegex, '');
// 删除PluginMap中的键
const pluginMapRegex = new RegExp(`\\s*${pluginMapEntry}`, 'g');
pluginDtsContent = pluginDtsContent.replace(pluginMapRegex, '');
// 格式化内容
pluginDtsContent = await this.formatContent(pluginDtsContent);
// 延迟2秒写入文件
setTimeout(async () => {
// 删除插件d.ts文件
if (fs.existsSync(pluginPath)) {
fs.unlink(pluginPath, () => {});
}
// 写入plugin.d.ts文件
fs.writeFile(pluginDtsPath, pluginDtsContent, () => {});
}, 2000);
}
/**
*
* @param content
*/
async formatContent(content: string) {
// 使用prettier格式化内容
return prettier.format(content, {
parser: 'typescript',
singleQuote: true,
trailingComma: 'all',
bracketSpacing: true,
arrowParens: 'avoid',
printWidth: 80,
});
}
/**
* d.ts文件
*/
async reGenerate() {
const pluginInfos = await this.pluginInfoEntity
.createQueryBuilder('a')
.where('a.status = :status', { status: 1 })
.select(['a.id', 'a.status', 'a.tsContent', 'a.keyName'])
.getMany();
for (const pluginInfo of pluginInfos) {
const data = await this.pluginService.getData(pluginInfo.keyName);
if (!data) {
continue;
}
const tsContent = data.tsContent?.data;
if (tsContent) {
await this.generateDtsFile(pluginInfo.keyName, tsContent);
await this.utils.sleep(200);
}
}
}
}

12
test/README.md Normal file
View File

@ -0,0 +1,12 @@
# 测试方式
考虑到cool-admin采用了自动化路由技术它与官方集成的jest测试工具并不兼容。为确保测试环境与实际的开发环境保持一致我们并不推荐使用jest进行测试。
# 自动化测试API工具
我们为您推荐以下的自动化API测试工具
- [Apifox](https://apifox.com/)
- [ApiPost](https://www.apipost.cn/)
同时这些工具也方便写API接口文档更加灵活有用

View File

@ -1,20 +0,0 @@
import { createApp, close, createHttpRequest } from '@midwayjs/mock';
import { Framework } from '@midwayjs/koa';
describe('test/controller/home.test.ts', () => {
it('should POST /api/get_user', async () => {
// create app
const app = await createApp<Framework>();
// make request
const result = await createHttpRequest(app).get('/api/get_user').query({ uid: 123 });
// use expect by jest
expect(result.status).toBe(200);
expect(result.body.message).toBe('OK');
// close app
await close(app);
});
});

View File

@ -1,21 +0,0 @@
import { createApp, close, createHttpRequest } from '@midwayjs/mock';
import { Framework } from '@midwayjs/koa';
describe('test/controller/home.test.ts', () => {
it('should GET /', async () => {
// create app
const app = await createApp<Framework>();
// make request
const result = await createHttpRequest(app).get('/');
// use expect by jest
expect(result.status).toBe(200);
expect(result.text).toBe('Hello Midwayjs!');
// close app
await close(app);
});
});

View File

@ -17,14 +17,14 @@
"noImplicitAny": false,
"typeRoots": [
"typings",
"./node_modules/@types"
"./node_modules/@types",
],
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
},
"exclude": [
"dist",
"node_modules",
"test"
]
}
"test",
],
}

8
typings/plugin.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import { BaseUpload, MODETYPE } from './upload';
type AnyString = string & {};
/**
*
*/
interface PluginMap {
upload: BaseUpload;
}

56
typings/upload.d.ts vendored Normal file
View File

@ -0,0 +1,56 @@
// 模式
export enum MODETYPE {
// 本地
LOCAL = 'local',
// 云存储
CLOUD = 'cloud',
// 其他
OTHER = 'other',
}
/**
*
*/
export interface Mode {
// 模式
mode: MODETYPE;
// 类型
type: string;
}
/**
*
*/
export interface BaseUpload {
/**
*
*/
getMode(): Promise<Mode>;
/**
*
* @returns
*/
getMetaFileObj(): Promise<any>;
/**
*
* @param url
* @param fileName
*/
downAndUpload(url: string, fileName?: string): Promise<string>;
/**
* Key()
* @param filePath
* @param key
*/
uploadWithKey(filePath, key): Promise<string>;
/**
*
* @param ctx
* @param key
*/
upload(ctx): Promise<string>;
}