mirror of
https://github.com/cool-team-official/cool-admin-midway.git
synced 2025-12-10 16:12:50 +00:00
兼容打包成二进制
This commit is contained in:
parent
fd8aaad60a
commit
fcef095b2a
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 文件上传
|
||||
|
||||
@ -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() {}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
];
|
||||
|
||||
13
src/index.ts
13
src/index.ts
@ -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';
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/modules/plugin/config.ts
Normal file
27
src/modules/plugin/config.ts
Normal 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;
|
||||
};
|
||||
56
src/modules/plugin/controller/admin/info.ts
Normal file
56
src/modules/plugin/controller/admin/info.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/modules/plugin/entity/info.ts
Normal file
44
src/modules/plugin/entity/info.ts
Normal 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;
|
||||
}
|
||||
44
src/modules/plugin/event/app.ts
Normal file
44
src/modules/plugin/event/app.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
36
src/modules/plugin/event/init.ts
Normal file
36
src/modules/plugin/event/init.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
26
src/modules/plugin/hooks/base.ts
Normal file
26
src/modules/plugin/hooks/base.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
120
src/modules/plugin/hooks/upload/index.ts
Normal file
120
src/modules/plugin/hooks/upload/index.ts
Normal 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;
|
||||
56
src/modules/plugin/hooks/upload/interface.ts
Normal file
56
src/modules/plugin/hooks/upload/interface.ts
Normal 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>;
|
||||
}
|
||||
25
src/modules/plugin/interface.ts
Normal file
25
src/modules/plugin/interface.ts
Normal 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;
|
||||
}
|
||||
200
src/modules/plugin/service/center.ts
Normal file
200
src/modules/plugin/service/center.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
447
src/modules/plugin/service/info.ts
Normal file
447
src/modules/plugin/service/info.ts
Normal 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`
|
||||
);
|
||||
}
|
||||
}
|
||||
256
src/modules/plugin/service/types.ts
Normal file
256
src/modules/plugin/service/types.ts
Normal 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
12
test/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# 测试方式
|
||||
|
||||
考虑到cool-admin采用了自动化路由技术,它与官方集成的jest测试工具并不兼容。为确保测试环境与实际的开发环境保持一致,我们并不推荐使用jest进行测试。
|
||||
|
||||
# 自动化测试API工具
|
||||
|
||||
我们为您推荐以下的自动化API测试工具:
|
||||
|
||||
- [Apifox](https://apifox.com/)
|
||||
- [ApiPost](https://www.apipost.cn/)
|
||||
|
||||
同时这些工具也方便写API接口文档,更加灵活有用
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
@ -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
8
typings/plugin.d.ts
vendored
Normal 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
56
typings/upload.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user