befly 2.3.2 → 3.0.0
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.
- package/apis/health/info.ts +64 -0
- package/apis/tool/tokenCheck.ts +51 -0
- package/bin/befly.ts +202 -0
- package/checks/conflict.ts +408 -0
- package/checks/table.ts +284 -0
- package/config/env.ts +218 -0
- package/config/reserved.ts +96 -0
- package/main.ts +101 -0
- package/package.json +45 -16
- package/plugins/{db.js → db.ts} +25 -12
- package/plugins/logger.ts +28 -0
- package/plugins/redis.ts +51 -0
- package/plugins/tool.ts +34 -0
- package/scripts/syncDb/apply.ts +171 -0
- package/scripts/syncDb/constants.ts +70 -0
- package/scripts/syncDb/ddl.ts +182 -0
- package/scripts/syncDb/helpers.ts +172 -0
- package/scripts/syncDb/index.ts +215 -0
- package/scripts/syncDb/schema.ts +199 -0
- package/scripts/syncDb/sqlite.ts +50 -0
- package/scripts/syncDb/state.ts +104 -0
- package/scripts/syncDb/table.ts +204 -0
- package/scripts/syncDb/tableCreate.ts +142 -0
- package/scripts/syncDb/tests/constants.test.ts +104 -0
- package/scripts/syncDb/tests/ddl.test.ts +134 -0
- package/scripts/syncDb/tests/helpers.test.ts +70 -0
- package/scripts/syncDb/types.ts +92 -0
- package/scripts/syncDb/version.ts +73 -0
- package/scripts/syncDb.ts +9 -0
- package/scripts/syncDev.ts +112 -0
- package/system.ts +149 -0
- package/tables/_common.json +21 -0
- package/tables/admin.json +10 -0
- package/tsconfig.json +58 -0
- package/types/api.d.ts +246 -0
- package/types/befly.d.ts +234 -0
- package/types/common.d.ts +215 -0
- package/types/context.ts +167 -0
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +278 -0
- package/types/index.d.ts +16 -0
- package/types/index.ts +459 -0
- package/types/jwt.d.ts +99 -0
- package/types/logger.d.ts +43 -0
- package/types/plugin.d.ts +109 -0
- package/types/redis.d.ts +44 -0
- package/types/tool.d.ts +67 -0
- package/types/validator.d.ts +45 -0
- package/utils/addonHelper.ts +60 -0
- package/utils/api.ts +23 -0
- package/utils/{colors.js → colors.ts} +79 -21
- package/utils/crypto.ts +308 -0
- package/utils/datetime.ts +51 -0
- package/utils/dbHelper.ts +142 -0
- package/utils/errorHandler.ts +68 -0
- package/utils/index.ts +46 -0
- package/utils/jwt.ts +493 -0
- package/utils/logger.ts +284 -0
- package/utils/objectHelper.ts +68 -0
- package/utils/pluginHelper.ts +62 -0
- package/utils/redisHelper.ts +338 -0
- package/utils/response.ts +38 -0
- package/utils/{sqlBuilder.js → sqlBuilder.ts} +233 -97
- package/utils/sqlHelper.ts +447 -0
- package/utils/tableHelper.ts +167 -0
- package/utils/tool.ts +230 -0
- package/utils/typeHelper.ts +101 -0
- package/utils/validate.ts +451 -0
- package/utils/{xml.js → xml.ts} +100 -74
- package/.npmrc +0 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -11
- package/apis/health/info.js +0 -49
- package/apis/tool/tokenCheck.js +0 -29
- package/checks/table.js +0 -221
- package/config/env.js +0 -62
- package/main.js +0 -579
- package/plugins/logger.js +0 -14
- package/plugins/redis.js +0 -32
- package/plugins/tool.js +0 -8
- package/scripts/syncDb.js +0 -603
- package/system.js +0 -118
- package/tables/common.json +0 -16
- package/tables/tool.json +0 -6
- package/utils/api.js +0 -27
- package/utils/crypto.js +0 -260
- package/utils/index.js +0 -387
- package/utils/jwt.js +0 -387
- package/utils/logger.js +0 -143
- package/utils/redisHelper.js +0 -74
- package/utils/sqlManager.js +0 -471
- package/utils/tool.js +0 -31
- package/utils/validate.js +0 -228
package/utils/logger.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志系统 - TypeScript 版本
|
|
3
|
+
* 提供分级日志记录和文件管理功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { appendFile, stat } from 'node:fs/promises';
|
|
8
|
+
import { formatDate } from './index.js';
|
|
9
|
+
import { Env } from '../config/env.js';
|
|
10
|
+
import type { LogLevel } from '../types/common.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 日志级别映射
|
|
14
|
+
*/
|
|
15
|
+
interface LogLevels {
|
|
16
|
+
error: number;
|
|
17
|
+
warn: number;
|
|
18
|
+
info: number;
|
|
19
|
+
debug: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 日志消息类型
|
|
24
|
+
*/
|
|
25
|
+
type LogMessage = string | number | boolean | null | undefined | Record<string, any> | any[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 日志器类
|
|
29
|
+
*/
|
|
30
|
+
export class Logger {
|
|
31
|
+
/** 当前日志级别 */
|
|
32
|
+
static level: LogLevel = (Env.LOG_LEVEL as LogLevel) || 'info';
|
|
33
|
+
|
|
34
|
+
/** 日志级别权重 */
|
|
35
|
+
static readonly levels: LogLevels = {
|
|
36
|
+
error: 0,
|
|
37
|
+
warn: 1,
|
|
38
|
+
info: 2,
|
|
39
|
+
debug: 3
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 日志目录 */
|
|
43
|
+
static logDir: string = Env.LOG_DIR || 'logs';
|
|
44
|
+
|
|
45
|
+
/** 单个日志文件最大大小(字节) */
|
|
46
|
+
static maxFileSize: number = Env.LOG_MAX_SIZE || 50 * 1024 * 1024; // 50MB
|
|
47
|
+
|
|
48
|
+
/** 当前使用的日志文件缓存 */
|
|
49
|
+
private static currentFiles: Map<string, string> = new Map();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 格式化日志消息
|
|
53
|
+
* @param level - 日志级别
|
|
54
|
+
* @param message - 日志消息
|
|
55
|
+
* @returns 格式化后的日志字符串
|
|
56
|
+
*/
|
|
57
|
+
static formatMessage(level: LogLevel, message: LogMessage): string {
|
|
58
|
+
const timestamp = formatDate();
|
|
59
|
+
const levelStr = level.toUpperCase().padStart(5);
|
|
60
|
+
|
|
61
|
+
let msg = `[${timestamp}] ${levelStr} - `;
|
|
62
|
+
|
|
63
|
+
// 处理不同类型的消息
|
|
64
|
+
if (typeof message === 'object' && message !== null) {
|
|
65
|
+
if (Object.keys(message).length > 0) {
|
|
66
|
+
msg += JSON.stringify(message).replace(/\s+/g, ' ').replace(/\\"/g, '"').replace(/\\n/g, ' ');
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
msg += String(message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return msg;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 检查是否应该记录该级别的日志
|
|
77
|
+
* @param level - 日志级别
|
|
78
|
+
* @returns 是否应该记录
|
|
79
|
+
*/
|
|
80
|
+
private static shouldLog(level: LogLevel): boolean {
|
|
81
|
+
return this.levels[level] <= this.levels[this.level];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 记录日志
|
|
86
|
+
* @param level - 日志级别
|
|
87
|
+
* @param message - 日志消息
|
|
88
|
+
*/
|
|
89
|
+
static async log(level: LogLevel, message: LogMessage): Promise<void> {
|
|
90
|
+
// 检查日志级别
|
|
91
|
+
if (!this.shouldLog(level)) return;
|
|
92
|
+
|
|
93
|
+
const formattedMessage = this.formatMessage(level, message);
|
|
94
|
+
|
|
95
|
+
// 控制台输出
|
|
96
|
+
if (Env.LOG_TO_CONSOLE === 1) {
|
|
97
|
+
console.log(formattedMessage);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await this.writeToFile(formattedMessage, level);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 写入日志文件
|
|
105
|
+
* @param message - 格式化后的消息
|
|
106
|
+
* @param level - 日志级别
|
|
107
|
+
*/
|
|
108
|
+
static async writeToFile(message: string, level: LogLevel = 'info'): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
let prefix: string;
|
|
111
|
+
|
|
112
|
+
// debug 日志使用单独的文件名
|
|
113
|
+
if (level === 'debug') {
|
|
114
|
+
prefix = 'debug';
|
|
115
|
+
} else {
|
|
116
|
+
prefix = new Date().toISOString().split('T')[0];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 检查缓存的当前文件是否仍然可用
|
|
120
|
+
let currentLogFile = this.currentFiles.get(prefix);
|
|
121
|
+
|
|
122
|
+
if (currentLogFile) {
|
|
123
|
+
try {
|
|
124
|
+
const stats = await stat(currentLogFile);
|
|
125
|
+
// 如果文件超过最大大小,清除缓存
|
|
126
|
+
if (stats.size >= this.maxFileSize) {
|
|
127
|
+
this.currentFiles.delete(prefix);
|
|
128
|
+
currentLogFile = undefined;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// 文件不存在或无法访问,清除缓存
|
|
132
|
+
this.currentFiles.delete(prefix);
|
|
133
|
+
currentLogFile = undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 如果没有缓存的文件或文件已满,查找合适的文件
|
|
138
|
+
if (!currentLogFile) {
|
|
139
|
+
currentLogFile = await this.findAvailableLogFile(prefix);
|
|
140
|
+
this.currentFiles.set(prefix, currentLogFile);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 使用 Node.js 的 appendFile 进行文件追加
|
|
144
|
+
await appendFile(currentLogFile, message + '\n', 'utf8');
|
|
145
|
+
} catch (error: any) {
|
|
146
|
+
console.error('写入日志文件失败:', error.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 查找可用的日志文件
|
|
152
|
+
* @param prefix - 文件前缀
|
|
153
|
+
* @returns 可用的日志文件路径
|
|
154
|
+
*/
|
|
155
|
+
static async findAvailableLogFile(prefix: string): Promise<string> {
|
|
156
|
+
const glob = new Bun.Glob(`${prefix}.*.log`);
|
|
157
|
+
const files = await Array.fromAsync(glob.scan(this.logDir));
|
|
158
|
+
|
|
159
|
+
// 按文件名排序
|
|
160
|
+
files.sort((a, b) => {
|
|
161
|
+
const aNum = parseInt(a.match(/\.(\d+)\.log$/)?.[1] || '0');
|
|
162
|
+
const bNum = parseInt(b.match(/\.(\d+)\.log$/)?.[1] || '0');
|
|
163
|
+
return aNum - bNum;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 从最后一个文件开始检查
|
|
167
|
+
for (let i = files.length - 1; i >= 0; i--) {
|
|
168
|
+
const filePath = path.join(this.logDir, files[i]);
|
|
169
|
+
try {
|
|
170
|
+
const stats = await stat(filePath);
|
|
171
|
+
if (stats.size < this.maxFileSize) {
|
|
172
|
+
return filePath;
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// 文件不存在或无法访问,跳过
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 所有文件都已满或没有文件,创建新文件
|
|
181
|
+
const existingIndices = files.map((f) => parseInt(f.match(/\.(\d+)\.log$/)?.[1] || '0'));
|
|
182
|
+
const nextIndex = existingIndices.length > 0 ? Math.max(...existingIndices) + 1 : 0;
|
|
183
|
+
|
|
184
|
+
return path.join(this.logDir, `${prefix}.${nextIndex}.log`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 记录错误日志
|
|
189
|
+
* @param message - 日志消息
|
|
190
|
+
*/
|
|
191
|
+
static async error(message: LogMessage): Promise<void> {
|
|
192
|
+
await this.log('error', message);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 记录警告日志
|
|
197
|
+
* @param message - 日志消息
|
|
198
|
+
*/
|
|
199
|
+
static async warn(message: LogMessage): Promise<void> {
|
|
200
|
+
await this.log('warn', message);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 记录信息日志
|
|
205
|
+
* @param message - 日志消息
|
|
206
|
+
*/
|
|
207
|
+
static async info(message: LogMessage): Promise<void> {
|
|
208
|
+
await this.log('info', message);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 记录调试日志(总是记录,忽略级别检查)
|
|
213
|
+
* @param message - 日志消息
|
|
214
|
+
*/
|
|
215
|
+
static async debug(message: LogMessage): Promise<void> {
|
|
216
|
+
const formattedMessage = this.formatMessage('debug', message);
|
|
217
|
+
|
|
218
|
+
// 控制台输出
|
|
219
|
+
if (Env.LOG_TO_CONSOLE === 1) {
|
|
220
|
+
console.log(formattedMessage);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await this.writeToFile(formattedMessage, 'debug');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 设置日志级别
|
|
228
|
+
* @param level - 新的日志级别
|
|
229
|
+
*/
|
|
230
|
+
static setLevel(level: LogLevel): void {
|
|
231
|
+
this.level = level;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 获取当前日志级别
|
|
236
|
+
* @returns 当前日志级别
|
|
237
|
+
*/
|
|
238
|
+
static getLevel(): LogLevel {
|
|
239
|
+
return this.level;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 设置日志目录
|
|
244
|
+
* @param dir - 新的日志目录
|
|
245
|
+
*/
|
|
246
|
+
static setLogDir(dir: string): void {
|
|
247
|
+
this.logDir = dir;
|
|
248
|
+
// 清除文件缓存
|
|
249
|
+
this.currentFiles.clear();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 设置最大文件大小
|
|
254
|
+
* @param size - 文件大小(字节)
|
|
255
|
+
*/
|
|
256
|
+
static setMaxFileSize(size: number): void {
|
|
257
|
+
this.maxFileSize = size;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 清除文件缓存
|
|
262
|
+
*/
|
|
263
|
+
static clearCache(): void {
|
|
264
|
+
this.currentFiles.clear();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 获取日志文件统计信息
|
|
269
|
+
* @returns 日志文件统计
|
|
270
|
+
*/
|
|
271
|
+
static getStats(): {
|
|
272
|
+
level: LogLevel;
|
|
273
|
+
logDir: string;
|
|
274
|
+
maxFileSize: number;
|
|
275
|
+
cachedFiles: number;
|
|
276
|
+
} {
|
|
277
|
+
return {
|
|
278
|
+
level: this.level,
|
|
279
|
+
logDir: this.logDir,
|
|
280
|
+
maxFileSize: this.maxFileSize,
|
|
281
|
+
cachedFiles: this.currentFiles.size
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly 对象操作工具
|
|
3
|
+
* 提供对象和数组的处理功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isType } from './typeHelper.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 挑选指定字段
|
|
10
|
+
* @param obj - 源对象
|
|
11
|
+
* @param keys - 要挑选的字段名数组
|
|
12
|
+
* @returns 包含指定字段的新对象
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* pickFields({ a: 1, b: 2, c: 3 }, ['a', 'c']) // { a: 1, c: 3 }
|
|
16
|
+
* pickFields({ name: 'John', age: 30 }, ['name']) // { name: 'John' }
|
|
17
|
+
*/
|
|
18
|
+
export const pickFields = <T extends Record<string, any>>(obj: T, keys: string[]): Partial<T> => {
|
|
19
|
+
if (!obj || (!isType(obj, 'object') && !isType(obj, 'array'))) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result: any = {};
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
if (key in obj) {
|
|
26
|
+
result[key] = obj[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 排除指定字段和值
|
|
35
|
+
* @param data - 源数据(对象或数组)
|
|
36
|
+
* @param excludeKeys - 要排除的字段名数组
|
|
37
|
+
* @param excludeValues - 要排除的值数组
|
|
38
|
+
* @returns 排除指定字段和值后的新数据
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* omitFields({ a: 1, b: 2, c: 3 }, ['b']) // { a: 1, c: 3 }
|
|
42
|
+
* omitFields({ a: 1, b: null, c: 3 }, [], [null]) // { a: 1, c: 3 }
|
|
43
|
+
* omitFields([{ a: 1 }, { a: 2 }], ['a']) // [{}, {}]
|
|
44
|
+
*/
|
|
45
|
+
export const omitFields = <T = any>(data: T, excludeKeys: string[] = [], excludeValues: any[] = []): T | Partial<T> => {
|
|
46
|
+
const shouldDropValue = (v: any): boolean => excludeValues.some((x) => x === v);
|
|
47
|
+
|
|
48
|
+
const cleanObject = (obj: any): any => {
|
|
49
|
+
if (!isType(obj, 'object')) return obj;
|
|
50
|
+
const result: any = {};
|
|
51
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
52
|
+
if (excludeKeys.includes(k)) continue;
|
|
53
|
+
if (shouldDropValue(v)) continue;
|
|
54
|
+
result[k] = v;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (isType(data, 'array')) {
|
|
60
|
+
return (data as any).filter((item: any) => !shouldDropValue(item)).map((item: any) => (isType(item, 'object') ? cleanObject(item) : item));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isType(data, 'object')) {
|
|
64
|
+
return cleanObject(data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return data;
|
|
68
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly 插件系统工具
|
|
3
|
+
* 提供插件依赖排序功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Plugin } from '../types/plugin.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 排序插件(根据依赖关系)
|
|
10
|
+
* 使用拓扑排序算法,确保依赖的插件先加载
|
|
11
|
+
*
|
|
12
|
+
* @param plugins - 插件数组
|
|
13
|
+
* @returns 排序后的插件数组,如果存在循环依赖则返回 false
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const plugins = [
|
|
17
|
+
* { name: 'logger', dependencies: [] },
|
|
18
|
+
* { name: 'db', dependencies: ['logger'] },
|
|
19
|
+
* { name: 'api', dependencies: ['db', 'logger'] }
|
|
20
|
+
* ];
|
|
21
|
+
*
|
|
22
|
+
* const sorted = sortPlugins(plugins);
|
|
23
|
+
* // [
|
|
24
|
+
* // { name: 'logger', dependencies: [] },
|
|
25
|
+
* // { name: 'db', dependencies: ['logger'] },
|
|
26
|
+
* // { name: 'api', dependencies: ['db', 'logger'] }
|
|
27
|
+
* // ]
|
|
28
|
+
*
|
|
29
|
+
* // 循环依赖示例
|
|
30
|
+
* const badPlugins = [
|
|
31
|
+
* { name: 'a', dependencies: ['b'] },
|
|
32
|
+
* { name: 'b', dependencies: ['a'] }
|
|
33
|
+
* ];
|
|
34
|
+
* sortPlugins(badPlugins); // false
|
|
35
|
+
*/
|
|
36
|
+
export const sortPlugins = (plugins: Plugin[]): Plugin[] | false => {
|
|
37
|
+
const result: Plugin[] = [];
|
|
38
|
+
const visited = new Set<string>();
|
|
39
|
+
const visiting = new Set<string>();
|
|
40
|
+
const pluginMap: Record<string, Plugin> = Object.fromEntries(plugins.map((p) => [p.name, p]));
|
|
41
|
+
let isPass = true;
|
|
42
|
+
|
|
43
|
+
const visit = (name: string): void => {
|
|
44
|
+
if (visited.has(name)) return;
|
|
45
|
+
if (visiting.has(name)) {
|
|
46
|
+
isPass = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const plugin = pluginMap[name];
|
|
51
|
+
if (!plugin) return; // 依赖不存在时跳过
|
|
52
|
+
|
|
53
|
+
visiting.add(name);
|
|
54
|
+
(plugin.dependencies || []).forEach(visit);
|
|
55
|
+
visiting.delete(name);
|
|
56
|
+
visited.add(name);
|
|
57
|
+
result.push(plugin);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
plugins.forEach((p) => visit(p.name));
|
|
61
|
+
return isPass ? result : false;
|
|
62
|
+
};
|