befly 3.8.18 → 3.8.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +1 -1
  3. package/lib/database.ts +28 -25
  4. package/lib/dbHelper.ts +3 -3
  5. package/lib/jwt.ts +90 -99
  6. package/lib/logger.ts +44 -23
  7. package/lib/redisHelper.ts +19 -22
  8. package/lib/validator.ts +11 -4
  9. package/loader/loadApis.ts +69 -114
  10. package/loader/loadHooks.ts +65 -0
  11. package/loader/loadPlugins.ts +50 -219
  12. package/main.ts +106 -133
  13. package/package.json +15 -7
  14. package/paths.ts +20 -0
  15. package/plugins/cache.ts +1 -3
  16. package/plugins/db.ts +8 -11
  17. package/plugins/logger.ts +5 -3
  18. package/plugins/redis.ts +10 -14
  19. package/router/api.ts +60 -106
  20. package/router/root.ts +15 -12
  21. package/router/static.ts +54 -58
  22. package/sync/syncAll.ts +58 -0
  23. package/sync/syncApi.ts +264 -0
  24. package/sync/syncDb/apply.ts +194 -0
  25. package/sync/syncDb/constants.ts +76 -0
  26. package/sync/syncDb/ddl.ts +194 -0
  27. package/sync/syncDb/helpers.ts +200 -0
  28. package/sync/syncDb/index.ts +164 -0
  29. package/sync/syncDb/schema.ts +201 -0
  30. package/sync/syncDb/sqlite.ts +50 -0
  31. package/sync/syncDb/table.ts +321 -0
  32. package/sync/syncDb/tableCreate.ts +146 -0
  33. package/sync/syncDb/version.ts +72 -0
  34. package/sync/syncDb.ts +19 -0
  35. package/sync/syncDev.ts +206 -0
  36. package/sync/syncMenu.ts +331 -0
  37. package/tsconfig.json +2 -4
  38. package/types/api.d.ts +6 -0
  39. package/types/befly.d.ts +152 -28
  40. package/types/context.d.ts +29 -3
  41. package/types/hook.d.ts +35 -0
  42. package/types/index.ts +14 -1
  43. package/types/plugin.d.ts +6 -7
  44. package/types/sync.d.ts +403 -0
  45. package/check.ts +0 -378
  46. package/env.ts +0 -106
  47. package/lib/middleware.ts +0 -275
  48. package/types/env.ts +0 -65
  49. package/types/util.d.ts +0 -45
  50. package/util.ts +0 -257
package/lib/middleware.ts DELETED
@@ -1,275 +0,0 @@
1
- /**
2
- * 中间件集合
3
- * 整合所有请求处理中间件
4
- */
5
-
6
- import { isEmpty, isPlainObject } from 'es-toolkit/compat';
7
- import { Env } from '../env.js';
8
- import { Logger } from './logger.js';
9
- import { Jwt } from './jwt.js';
10
- import { Xml } from './xml.js';
11
- import { Validator } from './validator.js';
12
- import { pickFields } from '../util.js';
13
- import type { ApiRoute } from '../types/api.js';
14
- import type { RequestContext } from '../types/context.js';
15
- import type { Plugin } from '../types/plugin.js';
16
- import type { BeflyContext } from '../types/befly.js';
17
-
18
- // ========================================
19
- // JWT 认证中间件
20
- // ========================================
21
-
22
- /**
23
- * 从请求头中提取并验证JWT token
24
- */
25
- export async function authenticate(ctx: RequestContext): Promise<void> {
26
- const authHeader = ctx.request.headers.get('authorization');
27
-
28
- if (authHeader && authHeader.startsWith('Bearer ')) {
29
- const token = authHeader.substring(7);
30
-
31
- try {
32
- const payload = await Jwt.verify(token);
33
- ctx.user = payload;
34
- } catch (error: any) {
35
- ctx.user = {};
36
- }
37
- } else {
38
- ctx.user = {};
39
- }
40
- }
41
-
42
- // ========================================
43
- // CORS 中间件
44
- // ========================================
45
-
46
- export interface CorsResult {
47
- headers: Record<string, string>;
48
- }
49
-
50
- /**
51
- * 设置 CORS 选项
52
- * 根据环境变量或请求头动态设置跨域配置
53
- *
54
- * 注意:Access-Control-Allow-Origin 只能返回单个源,不能用逗号分隔多个源
55
- * 如果配置了多个允许的源,需要根据请求的 Origin 动态返回匹配的源
56
- *
57
- * @param req - 请求对象
58
- * @returns CORS 配置对象
59
- */
60
- export const setCorsOptions = (req: Request): CorsResult => {
61
- return {
62
- headers: {
63
- 'Access-Control-Allow-Origin': Env.CORS_ALLOWED_ORIGIN === '*' ? req.headers.get('origin') : Env.CORS_ALLOWED_ORIGIN,
64
- 'Access-Control-Allow-Methods': Env.CORS_ALLOWED_METHODS,
65
- 'Access-Control-Allow-Headers': Env.CORS_ALLOWED_HEADERS,
66
- 'Access-Control-Expose-Headers': Env.CORS_EXPOSE_HEADERS,
67
- 'Access-Control-Max-Age': Env.CORS_MAX_AGE || 86400,
68
- 'Access-Control-Allow-Credentials': Env.CORS_ALLOW_CREDENTIALS || 'true'
69
- }
70
- };
71
- };
72
-
73
- /**
74
- * 处理OPTIONS预检请求
75
- * @param corsOptions - CORS 配置对象
76
- * @returns 204 响应
77
- */
78
- export function handleOptionsRequest(corsOptions: CorsResult): Response {
79
- return new Response(null, {
80
- status: 204,
81
- headers: corsOptions.headers
82
- });
83
- }
84
-
85
- // ========================================
86
- // 参数解析中间件
87
- // ========================================
88
-
89
- /**
90
- * 解析GET请求参数
91
- */
92
- export function parseGetParams(api: ApiRoute, ctx: RequestContext): void {
93
- const url = new URL(ctx.request.url);
94
-
95
- if (isPlainObject(api.fields) && !isEmpty(api.fields)) {
96
- ctx.body = pickFields(Object.fromEntries(url.searchParams), Object.keys(api.fields));
97
- } else {
98
- ctx.body = Object.fromEntries(url.searchParams);
99
- }
100
- }
101
-
102
- /**
103
- * 解析POST请求参数
104
- */
105
- export async function parsePostParams(api: ApiRoute, ctx: RequestContext): Promise<boolean> {
106
- try {
107
- const contentType = ctx.request.headers.get('content-type') || '';
108
-
109
- if (contentType.indexOf('json') !== -1) {
110
- try {
111
- ctx.body = await ctx.request.json();
112
- } catch (err) {
113
- // 如果没有 body 或 JSON 解析失败,默认为空对象
114
- ctx.body = {};
115
- }
116
- } else if (contentType.indexOf('xml') !== -1) {
117
- const textData = await ctx.request.text();
118
- const xmlData = Xml.parse(textData);
119
- ctx.body = xmlData?.xml ? xmlData.xml : xmlData;
120
- } else if (contentType.indexOf('form-data') !== -1) {
121
- ctx.body = await ctx.request.formData();
122
- } else if (contentType.indexOf('x-www-form-urlencoded') !== -1) {
123
- const text = await ctx.request.text();
124
- const formData = new URLSearchParams(text);
125
- ctx.body = Object.fromEntries(formData);
126
- } else {
127
- ctx.body = {};
128
- }
129
-
130
- if (isPlainObject(api.fields) && !isEmpty(api.fields)) {
131
- ctx.body = pickFields(ctx.body, Object.keys(api.fields));
132
- }
133
-
134
- return true;
135
- } catch (error: any) {
136
- Logger.error('处理请求参数时发生错误', error);
137
- return false;
138
- }
139
- }
140
-
141
- // ========================================
142
- // 权限验证中间件
143
- // ========================================
144
-
145
- export interface PermissionResult {
146
- code: 0 | 1;
147
- msg: string;
148
- data: any;
149
- [key: string]: any;
150
- }
151
-
152
- /**
153
- * 检查权限
154
- *
155
- * 验证流程:
156
- * 1. auth=false → 公开接口,直接通过
157
- * 2. auth=true → 需要登录认证
158
- * - 未登录 → 拒绝
159
- * - roleCode='dev' → 超级管理员,拥有所有权限
160
- * - 其他角色 → 检查接口是否在角色的可访问接口列表中(通过 Redis SISMEMBER 预判断)
161
- *
162
- * @param api - API 路由配置
163
- * @param ctx - 请求上下文
164
- * @param hasPermission - 是否有权限(通过 Redis SISMEMBER 预先判断)
165
- * @returns 统一的响应格式 { code: 0/1, msg, data, ...extra }
166
- */
167
- export function checkPermission(api: ApiRoute, ctx: RequestContext, hasPermission: boolean = false): PermissionResult {
168
- // 1. 公开接口(auth=false),无需验证
169
- if (api.auth === false) {
170
- return { code: 0, msg: '', data: {} };
171
- }
172
-
173
- // 2. 需要登录的接口(auth=true)
174
- // 2.1 检查是否登录
175
- if (!ctx.user?.id) {
176
- return {
177
- code: 1,
178
- msg: '未登录',
179
- data: {},
180
- login: 'no'
181
- };
182
- }
183
-
184
- // 2.2 dev 角色拥有所有权限(超级管理员)
185
- if (ctx.user.roleCode === 'dev') {
186
- return { code: 0, msg: '', data: {} };
187
- }
188
-
189
- // 2.3 检查接口权限(基于 Redis SISMEMBER 预判断结果)
190
- if (!hasPermission) {
191
- return {
192
- code: 1,
193
- msg: '没有权限访问此接口',
194
- data: {}
195
- };
196
- }
197
-
198
- // 验证通过
199
- return { code: 0, msg: '', data: {} };
200
- }
201
-
202
- // ========================================
203
- // 插件钩子中间件
204
- // ========================================
205
-
206
- /**
207
- * 执行所有插件的onGet钩子
208
- */
209
- export async function executePluginHooks(pluginLists: Plugin[], appContext: BeflyContext, ctx: RequestContext): Promise<void> {
210
- for await (const plugin of pluginLists) {
211
- try {
212
- if (typeof plugin?.onGet === 'function') {
213
- await plugin?.onGet(appContext, ctx, ctx.request);
214
- }
215
- } catch (error: any) {
216
- Logger.error('插件处理请求时发生错误', error);
217
- }
218
- }
219
- }
220
-
221
- // ========================================
222
- // 请求日志中间件
223
- // ========================================
224
-
225
- /**
226
- * 过滤日志字段(内部函数)
227
- * 用于从请求体中排除敏感字段(如密码、令牌等)
228
- * @param body - 请求体对象
229
- * @param excludeFields - 要排除的字段名(逗号分隔)
230
- * @returns 过滤后的对象
231
- */
232
- function filterLogFields(body: any, excludeFields: string = ''): any {
233
- if (!body || (!isPlainObject(body) && !Array.isArray(body))) return body;
234
-
235
- const fieldsArray = excludeFields
236
- .split(',')
237
- .map((field) => field.trim())
238
- .filter((field) => field.length > 0);
239
-
240
- const filtered: any = {};
241
- for (const [key, value] of Object.entries(body)) {
242
- if (!fieldsArray.includes(key)) {
243
- filtered[key] = value;
244
- }
245
- }
246
- return filtered;
247
- }
248
-
249
- /**
250
- * 记录请求日志
251
- */
252
- export function logRequest(apiPath: string, ctx: RequestContext): void {
253
- Logger.info({
254
- msg: '通用接口日志',
255
- 请求路径: apiPath,
256
- 请求方法: ctx.request.method,
257
- 用户信息: ctx.user,
258
- 请求参数: filterLogFields(ctx.body, Env.LOG_EXCLUDE_FIELDS),
259
- 耗时: Date.now() - ctx.startTime + 'ms'
260
- });
261
- }
262
-
263
- // ========================================
264
- // 参数验证中间件
265
- // ========================================
266
-
267
- // 创建全局validator实例
268
- const validator = new Validator();
269
-
270
- /**
271
- * 验证请求参数
272
- */
273
- export function validateParams(api: ApiRoute, ctx: RequestContext) {
274
- return validator.validate(ctx.body, api.fields, api.required);
275
- }
package/types/env.ts DELETED
@@ -1,65 +0,0 @@
1
- /**
2
- * 环境变量配置接口
3
- */
4
- export interface EnvConfig {
5
- // ========== 项目配置 ==========
6
- NODE_ENV: string;
7
- APP_NAME: string;
8
- APP_PORT: number;
9
- APP_HOST: string;
10
- DEV_EMAIL: string;
11
- DEV_PASSWORD: string;
12
- BODY_LIMIT: number;
13
- PARAMS_CHECK: string;
14
-
15
- // ========== 日志配置 ==========
16
- LOG_DEBUG: number;
17
- LOG_EXCLUDE_FIELDS: string;
18
- LOG_DIR: string;
19
- LOG_TO_CONSOLE: number;
20
- LOG_MAX_SIZE: number;
21
-
22
- // ========== 时区配置 ==========
23
- TZ: string;
24
-
25
- // ========== 数据库配置 ==========
26
- DATABASE_ENABLE: number;
27
- DB_TYPE: string;
28
- DB_HOST: string;
29
- DB_PORT: number;
30
- DB_USER: string;
31
- DB_PASS: string;
32
- DB_NAME: string;
33
- DB_POOL_MAX: number;
34
-
35
- // ========== Redis 配置 ==========
36
- REDIS_HOST: string;
37
- REDIS_PORT: number;
38
- REDIS_USERNAME: string;
39
- REDIS_PASSWORD: string;
40
- REDIS_DB: number;
41
- REDIS_KEY_PREFIX: string;
42
-
43
- // ========== JWT 配置 ==========
44
- JWT_SECRET: string;
45
- JWT_EXPIRES_IN: string;
46
- JWT_ALGORITHM: string;
47
-
48
- // ========== CORS 配置 ==========
49
- CORS_ALLOWED_ORIGIN: string;
50
- CORS_ALLOWED_METHODS: string;
51
- CORS_ALLOWED_HEADERS: string;
52
- CORS_EXPOSE_HEADERS: string;
53
- CORS_MAX_AGE: number;
54
- CORS_ALLOW_CREDENTIALS: string;
55
-
56
- // ========== 邮件配置 ==========
57
- MAIL_HOST: string;
58
- MAIL_PORT: number;
59
- MAIL_POOL: string;
60
- MAIL_SECURE: string;
61
- MAIL_USER: string;
62
- MAIL_PASS: string;
63
- MAIL_SENDER: string;
64
- MAIL_ADDRESS: string;
65
- }
package/types/util.d.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * util.ts 相关类型定义
3
- */
4
-
5
- import type { RedisClient } from 'bun';
6
- import type { DbHelper } from '../lib/dbHelper.js';
7
-
8
- /**
9
- * 数据清洗选项
10
- */
11
- export interface DataCleanOptions {
12
- /** 排除的字段名 */
13
- excludeKeys?: string[];
14
- /** 包含的字段名(优先级高于 excludeKeys) */
15
- includeKeys?: string[];
16
- /** 要移除的值 */
17
- removeValues?: any[];
18
- /** 字符串最大长度(超出截断) */
19
- maxLen?: number;
20
- /** 是否深度处理嵌套对象 */
21
- deep?: boolean;
22
- }
23
-
24
- /**
25
- * 数据库连接集合(内部使用)
26
- */
27
- export interface DatabaseConnections {
28
- redis: RedisClient | null;
29
- sql: any;
30
- helper: DbHelper | null;
31
- }
32
-
33
- /**
34
- * 请求上下文接口
35
- */
36
- export interface RequestContext {
37
- /** 请求体参数 */
38
- body: Record<string, any>;
39
- /** 用户信息 */
40
- user: Record<string, any>;
41
- /** 原始请求对象 */
42
- request: Request;
43
- /** 请求开始时间(毫秒) */
44
- startTime: number;
45
- }
package/util.ts DELETED
@@ -1,257 +0,0 @@
1
- import fs from 'node:fs';
2
- import { join } from 'pathe';
3
- import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
4
- import { isEmpty, isPlainObject } from 'es-toolkit/compat';
5
- import { snakeCase, camelCase, kebabCase } from 'es-toolkit/string';
6
- import { Env } from './env.js';
7
- import { Logger } from './lib/logger.js';
8
- import { projectDir, projectAddonsDir } from './paths.js';
9
- import type { KeyValue } from './types/common.js';
10
- import type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from './types/jwt';
11
- import type { Plugin } from './types/plugin.js';
12
-
13
- // ========================================
14
- // API 响应工具
15
- // ========================================
16
-
17
- /**
18
- * 成功响应
19
- */
20
- export const Yes = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 0; msg: string; data: T | {} } & KeyValue => {
21
- return {
22
- ...other,
23
- code: 0,
24
- msg: msg,
25
- data: data
26
- };
27
- };
28
-
29
- /**
30
- * 失败响应
31
- */
32
- export const No = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 1; msg: string; data: T | {} } & KeyValue => {
33
- return {
34
- ...other,
35
- code: 1,
36
- msg: msg,
37
- data: data
38
- };
39
- };
40
-
41
- // ========================================
42
- // 动态导入工具
43
- // ========================================
44
-
45
- // ========================================
46
- // 字段转换工具(重新导出 lib/convert.ts)
47
- // ========================================
48
-
49
- /**
50
- * 对象字段名转下划线
51
- * @param obj - 源对象
52
- * @returns 字段名转为下划线格式的新对象
53
- *
54
- * @example
55
- * keysToSnake({ userId: 123, userName: 'John' }) // { user_id: 123, user_name: 'John' }
56
- * keysToSnake({ createdAt: 1697452800000 }) // { created_at: 1697452800000 }
57
- */
58
- export const keysToSnake = <T = any>(obj: Record<string, any>): T => {
59
- if (!obj || !isPlainObject(obj)) return obj as T;
60
-
61
- const result: any = {};
62
- for (const [key, value] of Object.entries(obj)) {
63
- const snakeKey = snakeCase(key);
64
- result[snakeKey] = value;
65
- }
66
- return result;
67
- };
68
-
69
- /**
70
- * 对象字段名转小驼峰
71
- * @param obj - 源对象
72
- * @returns 字段名转为小驼峰格式的新对象
73
- *
74
- * @example
75
- * keysToCamel({ user_id: 123, user_name: 'John' }) // { userId: 123, userName: 'John' }
76
- * keysToCamel({ created_at: 1697452800000 }) // { createdAt: 1697452800000 }
77
- */
78
- export const keysToCamel = <T = any>(obj: Record<string, any>): T => {
79
- if (!obj || !isPlainObject(obj)) return obj as T;
80
-
81
- const result: any = {};
82
- for (const [key, value] of Object.entries(obj)) {
83
- const camelKey = camelCase(key);
84
- result[camelKey] = value;
85
- }
86
- return result;
87
- };
88
-
89
- /**
90
- * 数组对象字段名批量转小驼峰
91
- * @param arr - 源数组
92
- * @returns 字段名转为小驼峰格式的新数组
93
- *
94
- * @example
95
- * arrayKeysToCamel([
96
- * { user_id: 1, user_name: 'John' },
97
- * { user_id: 2, user_name: 'Jane' }
98
- * ])
99
- * // [{ userId: 1, userName: 'John' }, { userId: 2, userName: 'Jane' }]
100
- */
101
- export const arrayKeysToCamel = <T = any>(arr: Record<string, any>[]): T[] => {
102
- if (!arr || !Array.isArray(arr)) return arr as T[];
103
- return arr.map((item) => keysToCamel<T>(item));
104
- };
105
-
106
- // ========================================
107
- // 对象操作工具
108
- // ========================================
109
-
110
- /**
111
- * 挑选指定字段
112
- */
113
- export const pickFields = <T extends Record<string, any>>(obj: T, keys: string[]): Partial<T> => {
114
- if (!obj || (!isPlainObject(obj) && !Array.isArray(obj))) {
115
- return {};
116
- }
117
-
118
- const result: any = {};
119
- for (const key of keys) {
120
- if (key in obj) {
121
- result[key] = obj[key];
122
- }
123
- }
124
-
125
- return result;
126
- };
127
-
128
- /**
129
- * 字段清理
130
- */
131
- export const fieldClear = <T extends Record<string, any> = any>(data: T, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> => {
132
- if (!data || !isPlainObject(data)) {
133
- return {};
134
- }
135
-
136
- const result: any = {};
137
-
138
- for (const [key, value] of Object.entries(data)) {
139
- if (key in keepValues) {
140
- if (Object.is(keepValues[key], value)) {
141
- result[key] = value;
142
- continue;
143
- }
144
- }
145
-
146
- const shouldExclude = excludeValues.some((excludeVal) => Object.is(excludeVal, value));
147
- if (shouldExclude) {
148
- continue;
149
- }
150
-
151
- result[key] = value;
152
- }
153
-
154
- return result;
155
- };
156
-
157
- // ========================================
158
- // 日期时间工具
159
- // ========================================
160
-
161
- /**
162
- * 计算性能时间差
163
- */
164
- export const calcPerfTime = (startTime: number, endTime: number = Bun.nanoseconds()): string => {
165
- const elapsedMs = (endTime - startTime) / 1_000_000;
166
-
167
- if (elapsedMs < 1000) {
168
- return `${elapsedMs.toFixed(2)} 毫秒`;
169
- } else {
170
- const elapsedSeconds = elapsedMs / 1000;
171
- return `${elapsedSeconds.toFixed(2)} 秒`;
172
- }
173
- };
174
-
175
- // ========================================
176
- // Addon 工具函数
177
- // ========================================
178
-
179
- /**
180
- * 扫描所有可用的 addon
181
- * 优先从本地 addons/ 目录加载,其次从 node_modules/@befly-addon/ 加载
182
- * @returns addon 名称数组
183
- */
184
- export const scanAddons = (): string[] => {
185
- const addons = new Set<string>();
186
-
187
- // 1. 扫描本地 addons 目录(优先级高)
188
- // if (existsSync(projectAddonsDir)) {
189
- // try {
190
- // const localAddons = fs.readdirSync(projectAddonsDir).filter((name) => {
191
- // const fullPath = join(projectAddonsDir, name);
192
- // try {
193
- // const stat = statSync(fullPath);
194
- // return stat.isDirectory() && !name.startsWith('_');
195
- // } catch {
196
- // return false;
197
- // }
198
- // });
199
- // localAddons.forEach((name) => addons.add(name));
200
- // } catch (err) {
201
- // // 忽略本地目录读取错误
202
- // }
203
- // }
204
-
205
- // 2. 扫描 node_modules/@befly-addon 目录
206
- const beflyDir = join(projectDir, 'node_modules', '@befly-addon');
207
- if (existsSync(beflyDir)) {
208
- try {
209
- const npmAddons = fs.readdirSync(beflyDir).filter((name) => {
210
- // 如果本地已存在,跳过 npm 包版本
211
- if (addons.has(name)) return false;
212
-
213
- const fullPath = join(beflyDir, name);
214
- try {
215
- const stat = statSync(fullPath);
216
- return stat.isDirectory();
217
- } catch {
218
- return false;
219
- }
220
- });
221
- npmAddons.forEach((name) => addons.add(name));
222
- } catch {
223
- // 忽略 npm 目录读取错误
224
- }
225
- }
226
-
227
- return Array.from(addons).sort();
228
- };
229
-
230
- /**
231
- * 获取 addon 的指定子目录路径
232
- * 优先返回本地 addons 目录,其次返回 node_modules 目录
233
- * @param name - addon 名称
234
- * @param subDir - 子目录名称
235
- * @returns 完整路径
236
- */
237
- export const getAddonDir = (name: string, subDir: string): string => {
238
- // 优先使用本地 addons 目录
239
- // const localPath = join(projectAddonsDir, name, subDir);
240
- // if (existsSync(localPath)) {
241
- // return localPath;
242
- // }
243
-
244
- // 降级使用 node_modules 目录
245
- return join(projectDir, 'node_modules', '@befly-addon', name, subDir);
246
- };
247
-
248
- /**
249
- * 检查 addon 子目录是否存在
250
- * @param name - addon 名称
251
- * @param subDir - 子目录名称
252
- * @returns 是否存在
253
- */
254
- export const addonDirExists = (name: string, subDir: string): boolean => {
255
- const dir = getAddonDir(name, subDir);
256
- return existsSync(dir) && statSync(dir).isDirectory();
257
- };