From fcef095b2a4a2f7551cb3967ff676ab286c991d5 Mon Sep 17 00:00:00 2001 From: COOL Date: Wed, 15 Jan 2025 20:48:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=89=93=E5=8C=85=E6=88=90?= =?UTF-8?q?=E4=BA=8C=E8=BF=9B=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 9 +- src/comm/utils.ts | 18 +- src/config/config.default.ts | 8 +- src/configuration.ts | 13 +- src/entities.ts | 22 +- src/index.ts | 13 +- src/modules/base/controller/admin/comm.ts | 14 +- src/modules/base/controller/app/comm.ts | 14 +- src/modules/base/event/app.ts | 102 ----- src/modules/plugin/config.ts | 27 ++ src/modules/plugin/controller/admin/info.ts | 56 +++ src/modules/plugin/entity/info.ts | 44 ++ src/modules/plugin/event/app.ts | 44 ++ src/modules/plugin/event/init.ts | 36 ++ src/modules/plugin/hooks/base.ts | 26 ++ src/modules/plugin/hooks/upload/index.ts | 120 +++++ src/modules/plugin/hooks/upload/interface.ts | 56 +++ src/modules/plugin/interface.ts | 25 ++ src/modules/plugin/service/center.ts | 200 +++++++++ src/modules/plugin/service/info.ts | 447 +++++++++++++++++++ src/modules/plugin/service/types.ts | 256 +++++++++++ test/README.md | 12 + test/controller/api.test.ts | 20 - test/controller/home.test.ts | 21 - tsconfig.json | 10 +- typings/plugin.d.ts | 8 + typings/upload.d.ts | 56 +++ 27 files changed, 1485 insertions(+), 192 deletions(-) delete mode 100644 src/modules/base/event/app.ts create mode 100644 src/modules/plugin/config.ts create mode 100644 src/modules/plugin/controller/admin/info.ts create mode 100644 src/modules/plugin/entity/info.ts create mode 100644 src/modules/plugin/event/app.ts create mode 100644 src/modules/plugin/event/init.ts create mode 100644 src/modules/plugin/hooks/base.ts create mode 100644 src/modules/plugin/hooks/upload/index.ts create mode 100644 src/modules/plugin/hooks/upload/interface.ts create mode 100644 src/modules/plugin/interface.ts create mode 100644 src/modules/plugin/service/center.ts create mode 100644 src/modules/plugin/service/info.ts create mode 100644 src/modules/plugin/service/types.ts create mode 100644 test/README.md delete mode 100644 test/controller/api.test.ts delete mode 100644 test/controller/home.test.ts create mode 100644 typings/plugin.d.ts create mode 100644 typings/upload.d.ts diff --git a/package.json b/package.json index fb2887b..af31ab9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/comm/utils.ts b/src/comm/utils.ts index f213e95..075bb24 100644 --- a/src/comm/utils.ts +++ b/src/comm/utils.ts @@ -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 diff --git a/src/config/config.default.ts b/src/config/config.default.ts index f8d6519..338ba23 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -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(), + }, }, }, // 文件上传 diff --git a/src/configuration.ts b/src/configuration.ts index 79bd789..52420d4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -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() {} } diff --git a/src/entities.ts b/src/entities.ts index e02f396..d8028b0 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -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), ]; diff --git a/src/index.ts b/src/index.ts index def1566..c405fde 100644 --- a/src/index.ts +++ b/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'; diff --git a/src/modules/base/controller/admin/comm.ts b/src/modules/base/controller/admin/comm.ts index e5dd9e1..cf5548e 100644 --- a/src/modules/base/controller/admin/comm.ts +++ b/src/modules/base/controller/admin/comm.ts @@ -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()); } /** diff --git a/src/modules/base/controller/app/comm.ts b/src/modules/base/controller/app/comm.ts index 53e01b9..dc1b2c0 100644 --- a/src/modules/base/controller/app/comm.ts +++ b/src/modules/base/controller/app/comm.ts @@ -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()); } } diff --git a/src/modules/base/event/app.ts b/src/modules/base/event/app.ts deleted file mode 100644 index 508b79b..0000000 --- a/src/modules/base/event/app.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/modules/plugin/config.ts b/src/modules/plugin/config.ts new file mode 100644 index 0000000..74aa56b --- /dev/null +++ b/src/modules/plugin/config.ts @@ -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; +}; diff --git a/src/modules/plugin/controller/admin/info.ts b/src/modules/plugin/controller/admin/info.ts new file mode 100644 index 0000000..f8c9e15 --- /dev/null +++ b/src/modules/plugin/controller/admin/info.ts @@ -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) + ); + } +} diff --git a/src/modules/plugin/entity/info.ts b/src/modules/plugin/entity/info.ts new file mode 100644 index 0000000..2c75d57 --- /dev/null +++ b/src/modules/plugin/entity/info.ts @@ -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; +} diff --git a/src/modules/plugin/event/app.ts b/src/modules/plugin/event/app.ts new file mode 100644 index 0000000..2ef8f5c --- /dev/null +++ b/src/modules/plugin/event/app.ts @@ -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(); + } +} diff --git a/src/modules/plugin/event/init.ts b/src/modules/plugin/event/init.ts new file mode 100644 index 0000000..4a55409 --- /dev/null +++ b/src/modules/plugin/event/init.ts @@ -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); + } +} diff --git a/src/modules/plugin/hooks/base.ts b/src/modules/plugin/hooks/base.ts new file mode 100644 index 0000000..05aa206 --- /dev/null +++ b/src/modules/plugin/hooks/base.ts @@ -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; + } +} diff --git a/src/modules/plugin/hooks/upload/index.ts b/src/modules/plugin/hooks/upload/index.ts new file mode 100644 index 0000000..2c277e2 --- /dev/null +++ b/src/modules/plugin/hooks/upload/index.ts @@ -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; diff --git a/src/modules/plugin/hooks/upload/interface.ts b/src/modules/plugin/hooks/upload/interface.ts new file mode 100644 index 0000000..4f0cdfc --- /dev/null +++ b/src/modules/plugin/hooks/upload/interface.ts @@ -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; + + /** + * 获得原始操作对象 + * @returns + */ + getMetaFileObj(): Promise; + + /** + * 下载并上传 + * @param url + * @param fileName 文件名 + */ + downAndUpload(url: string, fileName?: string): Promise; + + /** + * 指定Key(路径)上传,本地文件上传到存储服务 + * @param filePath 文件路径 + * @param key 路径一致会覆盖源文件 + */ + uploadWithKey(filePath, key): Promise; + + /** + * 上传文件 + * @param ctx + * @param key 文件路径 + */ + upload(ctx): Promise; +} diff --git a/src/modules/plugin/interface.ts b/src/modules/plugin/interface.ts new file mode 100644 index 0000000..5af915d --- /dev/null +++ b/src/modules/plugin/interface.ts @@ -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; +} diff --git a/src/modules/plugin/service/center.ts b/src/modules/plugin/service/center.ts new file mode 100644 index 0000000..be94a9e --- /dev/null +++ b/src/modules/plugin/service/center.ts @@ -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 = new Map(); + + // 插件配置 + pluginInfos: Map = new Map(); + + @App() + app: IMidwayApplication; + + @InjectEntityModel(PluginInfoEntity) + pluginInfoEntity: Repository; + + @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; + } +} diff --git a/src/modules/plugin/service/info.ts b/src/modules/plugin/service/info.ts new file mode 100644 index 0000000..dd85475 --- /dev/null +++ b/src/modules/plugin/service/info.ts @@ -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; + + @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( + key: K | AnyString, + method: string, + ...params + ) { + // 实例 + const instance: any = await this.getInstance(key); + return await instance[method](...params); + } + + /** + * 获得插件实例 + * @param key + * @returns + */ + async getInstance( + key: K | AnyString + ): Promise { + 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` + ); + } +} diff --git a/src/modules/plugin/service/types.ts b/src/modules/plugin/service/types.ts new file mode 100644 index 0000000..1a45cdc --- /dev/null +++ b/src/modules/plugin/service/types.ts @@ -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; + + @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); + } + } + } +} diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..7effc7e --- /dev/null +++ b/test/README.md @@ -0,0 +1,12 @@ +# 测试方式 + +考虑到cool-admin采用了自动化路由技术,它与官方集成的jest测试工具并不兼容。为确保测试环境与实际的开发环境保持一致,我们并不推荐使用jest进行测试。 + +# 自动化测试API工具 + +我们为您推荐以下的自动化API测试工具: + +- [Apifox](https://apifox.com/) +- [ApiPost](https://www.apipost.cn/) + +同时这些工具也方便写API接口文档,更加灵活有用 \ No newline at end of file diff --git a/test/controller/api.test.ts b/test/controller/api.test.ts deleted file mode 100644 index da433b5..0000000 --- a/test/controller/api.test.ts +++ /dev/null @@ -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(); - - // 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); - }); -}); diff --git a/test/controller/home.test.ts b/test/controller/home.test.ts deleted file mode 100644 index 93dc66f..0000000 --- a/test/controller/home.test.ts +++ /dev/null @@ -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(); - - // 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); - }); - -}); diff --git a/tsconfig.json b/tsconfig.json index 9e88494..00074bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" - ] -} \ No newline at end of file + "test", + ], +} diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts new file mode 100644 index 0000000..e2ae5a0 --- /dev/null +++ b/typings/plugin.d.ts @@ -0,0 +1,8 @@ +import { BaseUpload, MODETYPE } from './upload'; +type AnyString = string & {}; +/** + * 插件类型声明 + */ +interface PluginMap { + upload: BaseUpload; +} diff --git a/typings/upload.d.ts b/typings/upload.d.ts new file mode 100644 index 0000000..4f0cdfc --- /dev/null +++ b/typings/upload.d.ts @@ -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; + + /** + * 获得原始操作对象 + * @returns + */ + getMetaFileObj(): Promise; + + /** + * 下载并上传 + * @param url + * @param fileName 文件名 + */ + downAndUpload(url: string, fileName?: string): Promise; + + /** + * 指定Key(路径)上传,本地文件上传到存储服务 + * @param filePath 文件路径 + * @param key 路径一致会覆盖源文件 + */ + uploadWithKey(filePath, key): Promise; + + /** + * 上传文件 + * @param ctx + * @param key 文件路径 + */ + upload(ctx): Promise; +}