befly 3.3.5 → 3.4.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.
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { relative, basename } from 'pathe';
7
7
  import { Logger } from '../lib/logger.js';
8
- import { paths } from '../paths.js';
8
+ import { projectPluginDir, coreTableDir, projectTableDir, projectApiDir } from '../paths.js';
9
9
  import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
10
10
 
11
11
  /**
@@ -41,7 +41,7 @@ async function collectCorePlugins(registry: ResourceRegistry): Promise<void> {
41
41
  try {
42
42
  const glob = new Bun.Glob('*.ts');
43
43
  for await (const file of glob.scan({
44
- cwd: paths.projectPluginDir,
44
+ cwd: projectPluginDir,
45
45
  onlyFiles: true,
46
46
  absolute: true
47
47
  })) {
@@ -77,7 +77,7 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
77
77
  const glob = new Bun.Glob('*.json');
78
78
 
79
79
  for await (const file of glob.scan({
80
- cwd: paths.rootTableDir,
80
+ cwd: addonTablesDir,
81
81
  onlyFiles: true,
82
82
  absolute: true
83
83
  })) {
@@ -183,7 +183,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
183
183
  const conflicts: string[] = [];
184
184
 
185
185
  // 收集用户表定义
186
- const userTablesDir = paths.projectTableDir;
186
+ const userTablesDir = projectTableDir;
187
187
  try {
188
188
  const glob = new Bun.Glob('*.json');
189
189
  for await (const file of glob.scan({
@@ -219,7 +219,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
219
219
  }
220
220
 
221
221
  // 收集用户 API 路由
222
- const userApisDir = paths.projectApiDir;
222
+ const userApisDir = projectApiDir;
223
223
  try {
224
224
  const glob = new Bun.Glob('**/*.ts');
225
225
  for await (const file of glob.scan({
@@ -255,7 +255,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
255
255
  }
256
256
 
257
257
  // 收集用户插件
258
- const userPluginsDir = paths.projectPluginDir;
258
+ const userPluginsDir = projectPluginDir;
259
259
  try {
260
260
  const glob = new Bun.Glob('*.ts');
261
261
  for await (const file of glob.scan({
package/checks/table.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { basename } from 'pathe';
7
7
  import { Logger } from '../lib/logger.js';
8
8
  import { parseRule } from '../util.js';
9
- import { paths } from '../paths.js';
9
+ import { projectTableDir } from '../paths.js';
10
10
  import { scanAddons, getAddonDir } from '../util.js';
11
11
 
12
12
  /**
@@ -69,7 +69,7 @@ export default async function (): Promise<boolean> {
69
69
 
70
70
  // 收集项目表字段定义文件
71
71
  for await (const file of tablesGlob.scan({
72
- cwd: paths.projectTableDir,
72
+ cwd: projectTableDir,
73
73
  absolute: true,
74
74
  onlyFiles: true
75
75
  })) {
package/commands/build.ts CHANGED
@@ -28,20 +28,24 @@ interface BuildOptions {
28
28
  export async function buildCommand(options: BuildOptions) {
29
29
  try {
30
30
  const projectRoot = getProjectRoot();
31
- const mainFile = join(projectRoot, 'main.ts');
32
31
 
33
- if (!existsSync(mainFile)) {
34
- Logger.error('未找到 main.ts 文件');
32
+ // 验证是否在项目目录下
33
+ const packageJsonPath = join(projectRoot, 'package.json');
34
+ if (!existsSync(packageJsonPath)) {
35
+ Logger.error('未找到 package.json 文件,请确保在项目目录下');
35
36
  process.exit(1);
36
37
  }
37
38
 
39
+ // 使用内置默认入口文件
40
+ const entryFile = join(import.meta.dir, '..', 'entry.ts');
41
+
38
42
  const spinner = ora({
39
43
  text: '正在构建项目...',
40
44
  color: 'cyan',
41
45
  spinner: 'dots'
42
46
  }).start();
43
47
 
44
- const args = ['build', mainFile, '--outdir', options.outdir, '--target', 'bun'];
48
+ const args = ['build', entryFile, '--outdir', options.outdir, '--target', 'bun'];
45
49
 
46
50
  if (options.minify) {
47
51
  args.push('--minify');
package/commands/dev.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import { join } from 'pathe';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { Logger } from '../lib/logger.js';
8
+ import { Befly } from '../main.js';
8
9
 
9
10
  interface DevOptions {
10
11
  port: string;
@@ -27,10 +28,11 @@ function getProjectRoot(): string {
27
28
  export async function devCommand(options: DevOptions) {
28
29
  try {
29
30
  const projectRoot = getProjectRoot();
30
- const mainFile = join(projectRoot, 'main.ts');
31
31
 
32
- if (!existsSync(mainFile)) {
33
- Logger.error('未找到 main.ts 文件,请确保在 Befly 项目目录下');
32
+ // 验证是否在 Befly 项目目录下
33
+ const packageJsonPath = join(projectRoot, 'package.json');
34
+ if (!existsSync(packageJsonPath)) {
35
+ Logger.error('未找到 package.json 文件,请确保在项目目录下');
34
36
  process.exit(1);
35
37
  }
36
38
 
@@ -54,38 +56,25 @@ export async function devCommand(options: DevOptions) {
54
56
  Logger.info(`环境变量文件: .env.development\n`);
55
57
  }
56
58
 
57
- // 使用 Bun.spawn 启动开发服务器(不使用 --watch 避免监听 node_modules)
59
+ // 直接启动 Befly 实例
60
+ const app = new Befly();
61
+ const server = await app.listen();
58
62
 
59
- const proc = Bun.spawn(['bun', '--env-file=.env.development', 'run', mainFile], {
60
- cwd: projectRoot,
61
- stdout: 'inherit',
62
- stderr: 'inherit',
63
- stdin: 'inherit',
64
- env: {
65
- // ...process.env,
66
- // NODE_ENV: 'development',
67
- // APP_PORT: options.port,
68
- // APP_HOST: options.host,
69
- // LOG_DEBUG: options.verbose ? '1' : process.env.LOG_DEBUG,
70
- // FORCE_COLOR: '1'
71
- }
72
- });
73
-
74
- // 添加信号处理,确保优雅关闭
75
- const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];
63
+ // 设置信号处理,确保优雅关闭
64
+ const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
76
65
  signals.forEach((signal) => {
77
- process.on(signal, () => {
78
- console.log(`\nShutting down dev server (${signal})...`);
79
- proc.kill(signal);
80
- setTimeout(() => {
81
- proc.kill('SIGKILL');
66
+ process.on(signal, async () => {
67
+ Logger.info(`\n收到 ${signal} 信号,正在关闭开发服务器...`);
68
+ try {
69
+ server.stop(true);
70
+ Logger.info('开发服务器已关闭');
71
+ process.exit(0);
72
+ } catch (error) {
73
+ Logger.error('关闭开发服务器失败:', error);
82
74
  process.exit(1);
83
- }, 5000); // 5 秒强制关闭
75
+ }
84
76
  });
85
77
  });
86
-
87
- const exitCode = await proc.exited;
88
- process.exit(exitCode || 0);
89
78
  } catch (error) {
90
79
  Logger.error('启动开发服务器失败:');
91
80
  console.error(error);
package/commands/start.ts CHANGED
@@ -6,6 +6,7 @@ import { join } from 'pathe';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { Logger } from '../lib/logger.js';
8
8
  import { ClusterManager } from '../lifecycle/cluster.js';
9
+ import { Befly } from '../main.js';
9
10
 
10
11
  function getProjectRoot(): string {
11
12
  let current = process.cwd();
@@ -28,10 +29,11 @@ interface StartOptions {
28
29
  export async function startCommand(options: StartOptions) {
29
30
  try {
30
31
  const projectRoot = getProjectRoot();
31
- const mainFile = join(projectRoot, 'main.ts');
32
32
 
33
- if (!existsSync(mainFile)) {
34
- Logger.error('未找到 main.ts 文件');
33
+ // 验证是否在项目目录下
34
+ const packageJsonPath = join(projectRoot, 'package.json');
35
+ if (!existsSync(packageJsonPath)) {
36
+ Logger.error('未找到 package.json 文件,请确保在项目目录下');
35
37
  process.exit(1);
36
38
  }
37
39
 
@@ -49,8 +51,7 @@ export async function startCommand(options: StartOptions) {
49
51
  instances,
50
52
  startPort: parseInt(options.port),
51
53
  host: options.host,
52
- projectRoot,
53
- mainFile
54
+ projectRoot
54
55
  });
55
56
 
56
57
  await clusterManager.start();
@@ -67,10 +68,25 @@ export async function startCommand(options: StartOptions) {
67
68
  Logger.info(`环境变量文件: .env.production\n`);
68
69
  }
69
70
 
70
- // 直接导入并运行 main.ts(Bun 会自动加载 .env.production)
71
- await import(mainFile);
71
+ // 直接启动 Befly 实例
72
+ const app = new Befly();
73
+ const server = await app.listen();
72
74
 
73
- // 注意:正常情况下不会执行到这里,因为 main.ts 会启动服务器并持续运行
75
+ // 设置信号处理,确保优雅关闭
76
+ const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
77
+ signals.forEach((signal) => {
78
+ process.on(signal, async () => {
79
+ Logger.info(`\n收到 ${signal} 信号,正在关闭生产服务器...`);
80
+ try {
81
+ server.stop(true);
82
+ Logger.info('生产服务器已关闭');
83
+ process.exit(0);
84
+ } catch (error) {
85
+ Logger.error('关闭生产服务器失败:', error);
86
+ process.exit(1);
87
+ }
88
+ });
89
+ });
74
90
  }
75
91
  } catch (error) {
76
92
  Logger.error('启动失败:');
@@ -14,7 +14,7 @@ import { Env } from '../../config/env.js';
14
14
  import { scanAddons, addonDirExists, getAddonDir } from '../../util.js';
15
15
  import { Database } from '../../lib/database.js';
16
16
  import checkTable from '../../checks/table.js';
17
- import { paths } from '../../paths.js';
17
+ import { coreTableDir, projectTableDir } from '../../paths.js';
18
18
 
19
19
  // 导入模块化的功能
20
20
  import { ensureDbVersion } from './version.js';
@@ -81,9 +81,9 @@ export const SyncDb = async (): Promise<void> => {
81
81
  const tablesGlob = new Bun.Glob('*.json');
82
82
  const directories: Array<{ path: string; type: 'core' | 'app' | 'addon'; addonName?: string }> = [
83
83
  // 1. core 框架表(core_ 前缀)
84
- { path: paths.coreTableDir, type: 'core' },
84
+ { path: coreTableDir, type: 'core' },
85
85
  // 2. 项目表(无前缀)
86
- { path: paths.projectTableDir, type: 'app' }
86
+ { path: projectTableDir, type: 'app' }
87
87
  ];
88
88
 
89
89
  // 添加所有 addon 的 tables 目录(addon_{name}_ 前缀)
@@ -80,7 +80,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
80
80
 
81
81
  if (isStringOrArrayType(fieldType) && existingColumns[dbFieldName].length) {
82
82
  if (existingColumns[dbFieldName].length! > fieldMax) {
83
- Logger.warn(`[跳过危险变更] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].length} -> ${fieldMax} 已被跳过(设置 SYNC_DISALLOW_SHRINK=0 可放开)`);
83
+ Logger.warn(`[跳过危险变更] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].length} -> ${fieldMax} 已被跳过`);
84
84
  }
85
85
  }
86
86
 
@@ -3,13 +3,14 @@
3
3
  * 说明:根据 menu.json 配置文件增量同步菜单数据(最多2级:父级和子级)
4
4
  *
5
5
  * 流程:
6
- * 1. 读取 core tpl 两个配置文件,core 优先覆盖 tpl
7
- * 2. 合并菜单配置(根据 path 匹配,core 覆盖 tpl)
8
- * 3. 子级菜单自动追加父级路径作为前缀
9
- * 4. 根据菜单的 path 字段检查是否存在
10
- * 5. 存在则更新其他字段(name、icon、sort、type、pid)
11
- * 6. 不存在则新增菜单记录
12
- * 7. 强制删除配置中不存在的菜单记录
6
+ * 1. 读取 core/config/menu.json 和项目根目录的 menu.json 配置文件
7
+ * 2. core 配置优先覆盖项目配置(根据 path 匹配)
8
+ * 3. 文件不存在或格式错误时默认为空数组
9
+ * 4. 子级菜单自动追加父级路径作为前缀
10
+ * 5. 根据菜单的 path 字段检查是否存在
11
+ * 6. 存在则更新其他字段(name、icon、sort、type、pid)
12
+ * 7. 不存在则新增菜单记录
13
+ * 8. 强制删除配置中不存在的菜单记录
13
14
  * 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
14
15
  */
15
16
 
@@ -17,7 +18,7 @@ import { Logger } from '../lib/logger.js';
17
18
  import { Database } from '../lib/database.js';
18
19
  import { join } from 'pathe';
19
20
  import { existsSync } from 'node:fs';
20
- import { paths } from '../paths.js';
21
+ import { coreDIr, projectDir } from '../paths.js';
21
22
 
22
23
  interface SyncMenuOptions {
23
24
  plan?: boolean;
@@ -34,35 +35,46 @@ interface MenuConfig {
34
35
 
35
36
  /**
36
37
  * 读取菜单配置文件
38
+ * 如果文件不存在或不是数组格式,返回空数组
37
39
  */
38
40
  async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
39
41
  try {
40
42
  if (!existsSync(filePath)) {
43
+ Logger.warn(`菜单配置文件不存在: ${filePath},使用空数组`);
41
44
  return [];
42
45
  }
46
+
43
47
  const file = Bun.file(filePath);
44
- return await file.json();
48
+ const content = await file.json();
49
+
50
+ // 验证是否为数组
51
+ if (!Array.isArray(content)) {
52
+ Logger.warn(`菜单配置文件格式错误(非数组): ${filePath},使用空数组`);
53
+ return [];
54
+ }
55
+
56
+ return content;
45
57
  } catch (error: any) {
46
- Logger.warn(`读取菜单配置失败: ${filePath}`, error.message);
58
+ Logger.warn(`读取菜单配置失败: ${filePath},使用空数组`, error.message);
47
59
  return [];
48
60
  }
49
61
  }
50
62
 
51
63
  /**
52
- * 合并菜单配置(core 优先覆盖 tpl)
64
+ * 合并菜单配置(core 优先覆盖项目)
53
65
  * 支持二级菜单结构:父级和子级
54
66
  */
55
- function mergeMenuConfigs(tplMenus: MenuConfig[], coreMenus: MenuConfig[]): MenuConfig[] {
67
+ function mergeMenuConfigs(projectMenus: MenuConfig[], coreMenus: MenuConfig[]): MenuConfig[] {
56
68
  const menuMap = new Map<string, MenuConfig>();
57
69
 
58
- // 1. 先添加 tpl 菜单
59
- for (const menu of tplMenus) {
70
+ // 1. 先添加项目菜单
71
+ for (const menu of projectMenus) {
60
72
  if (menu.path) {
61
73
  menuMap.set(menu.path, { ...menu });
62
74
  }
63
75
  }
64
76
 
65
- // 2. core 菜单覆盖同 path 的 tpl 菜单
77
+ // 2. core 菜单覆盖同 path 的项目菜单
66
78
  for (const menu of coreMenus) {
67
79
  if (menu.path) {
68
80
  menuMap.set(menu.path, { ...menu });
@@ -78,10 +90,10 @@ function mergeMenuConfigs(tplMenus: MenuConfig[], coreMenus: MenuConfig[]): Menu
78
90
  if (menu.children && menu.children.length > 0) {
79
91
  const childMap = new Map<string, MenuConfig>();
80
92
 
81
- // 先添加 tpl 的子菜单
82
- const tplMenu = tplMenus.find((m) => m.path === menu.path);
83
- if (tplMenu?.children) {
84
- for (const child of tplMenu.children) {
93
+ // 先添加项目的子菜单
94
+ const projectMenu = projectMenus.find((m) => m.path === menu.path);
95
+ if (projectMenu?.children) {
96
+ for (const child of projectMenu.children) {
85
97
  if (child.path) {
86
98
  childMap.set(child.path, { ...child });
87
99
  }
@@ -269,8 +281,8 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}) {
269
281
  try {
270
282
  if (options.plan) {
271
283
  Logger.info('[计划] 同步菜单配置到数据库(plan 模式不执行)');
272
- Logger.info('[计划] 1. 读取 core tpl 两个配置文件');
273
- Logger.info('[计划] 2. 合并菜单配置(core 优先覆盖 tpl)');
284
+ Logger.info('[计划] 1. 读取 core/config/menu.json 和项目根目录 menu.json');
285
+ Logger.info('[计划] 2. 合并菜单配置(core 优先覆盖项目)');
274
286
  Logger.info('[计划] 3. 子级菜单自动追加父级路径前缀');
275
287
  Logger.info('[计划] 4. 根据 path 检查菜单是否存在');
276
288
  Logger.info('[计划] 5. 存在则更新,不存在则新增');
@@ -283,21 +295,21 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}) {
283
295
 
284
296
  // 1. 读取两个配置文件
285
297
  Logger.info('=== 步骤 1: 读取菜单配置文件 ===');
286
- const tplMenuPath = join(paths.projectConfigDir, 'menu.json');
287
- const coreMenuPath = join(paths.rootConfigDir, 'menu.json');
298
+ const projectMenuPath = join(projectDir, 'menu.json');
299
+ const coreMenuPath = join(coreDIr, 'menu.json');
288
300
 
289
- Logger.info(` tpl 路径: ${tplMenuPath}`);
301
+ Logger.info(` 项目路径: ${projectMenuPath}`);
290
302
  Logger.info(` core 路径: ${coreMenuPath}`);
291
303
 
292
- const tplMenus = await readMenuConfig(tplMenuPath);
304
+ const projectMenus = await readMenuConfig(projectMenuPath);
293
305
  const coreMenus = await readMenuConfig(coreMenuPath);
294
306
 
295
- Logger.info(`✅ tpl 配置: ${tplMenus.length} 个父级菜单`);
307
+ Logger.info(`✅ 项目配置: ${projectMenus.length} 个父级菜单`);
296
308
  Logger.info(`✅ core 配置: ${coreMenus.length} 个父级菜单`);
297
309
 
298
310
  // 2. 合并菜单配置
299
- Logger.info('\n=== 步骤 2: 合并菜单配置(core 优先覆盖 tpl) ===');
300
- const mergedMenus = mergeMenuConfigs(tplMenus, coreMenus);
311
+ Logger.info('\n=== 步骤 2: 合并菜单配置(core 优先覆盖项目) ===');
312
+ const mergedMenus = mergeMenuConfigs(projectMenus, coreMenus);
301
313
  Logger.info(`✅ 合并后共有 ${mergedMenus.length} 个父级菜单`);
302
314
 
303
315
  // 打印合并后的菜单结构
package/config/env.ts CHANGED
@@ -116,16 +116,6 @@ export interface EnvConfig {
116
116
  MAIL_SENDER: string;
117
117
  /** 发件人地址 */
118
118
  MAIL_ADDRESS: string;
119
-
120
- // ========== 同步脚本配置 ==========
121
- /** 是否合并 ALTER 语句 */
122
- SYNC_MERGE_ALTER: string;
123
- /** 是否同步在线索引 */
124
- SYNC_ONLINE_INDEX: string;
125
- /** 是否禁止字段缩小 */
126
- SYNC_DISALLOW_SHRINK: string;
127
- /** 是否允许类型变更 */
128
- SYNC_ALLOW_TYPE_CHANGE: string;
129
119
  }
130
120
 
131
121
  /**
@@ -208,11 +198,5 @@ export const Env: EnvConfig = {
208
198
  MAIL_USER: getEnv('MAIL_USER', ''),
209
199
  MAIL_PASS: getEnv('MAIL_PASS', ''),
210
200
  MAIL_SENDER: getEnv('MAIL_SENDER', ''),
211
- MAIL_ADDRESS: getEnv('MAIL_ADDRESS', ''),
212
-
213
- // ========== 同步脚本配置 ==========
214
- SYNC_MERGE_ALTER: getEnv('SYNC_MERGE_ALTER', 'false'),
215
- SYNC_ONLINE_INDEX: getEnv('SYNC_ONLINE_INDEX', 'false'),
216
- SYNC_DISALLOW_SHRINK: getEnv('SYNC_DISALLOW_SHRINK', 'true'),
217
- SYNC_ALLOW_TYPE_CHANGE: getEnv('SYNC_ALLOW_TYPE_CHANGE', 'false')
201
+ MAIL_ADDRESS: getEnv('MAIL_ADDRESS', '')
218
202
  };
@@ -6,7 +6,7 @@
6
6
  import { join, basename } from 'pathe';
7
7
  import { Logger } from '../lib/logger.js';
8
8
  import { calcPerfTime } from '../util.js';
9
- import { paths } from '../paths.js';
9
+ import { coreCheckDir } from '../paths.js';
10
10
  import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
11
11
 
12
12
  /**
@@ -31,7 +31,7 @@ export class Checker {
31
31
 
32
32
  // 1. 优先执行资源冲突检测(如果存在)
33
33
  try {
34
- const conflictCheckPath = join(paths.rootCheckDir, 'conflict.ts');
34
+ const conflictCheckPath = join(coreCheckDir, 'conflict.ts');
35
35
  const conflictCheckFile = Bun.file(conflictCheckPath);
36
36
 
37
37
  if (await conflictCheckFile.exists()) {
@@ -71,7 +71,7 @@ export class Checker {
71
71
 
72
72
  // 2. 检查目录列表:先核心,后项目,最后 addons
73
73
  // 检查所有 checks 目录
74
- const checkDirs = [{ path: paths.rootCheckDir, type: 'core' as const }]; // 添加所有 addon 的 checks 目录
74
+ const checkDirs = [{ path: coreCheckDir, type: 'core' as const }]; // 添加所有 addon 的 checks 目录
75
75
  const addons = scanAddons();
76
76
  for (const addon of addons) {
77
77
  if (addonDirExists(addon, 'checks')) {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Cluster Worker 入口
3
+ * 由 ClusterManager 启动的子进程入口文件
4
+ */
5
+
6
+ import { Befly } from '../main.js';
7
+ import { Logger } from '../lib/logger.js';
8
+
9
+ // 启动 Befly 实例
10
+ const app = new Befly();
11
+ const server = await app.listen();
12
+
13
+ // Bun 原生信号处理:当收到 SIGTERM/SIGINT 时优雅关闭
14
+ const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
15
+
16
+ signals.forEach((signal) => {
17
+ process.on(signal, async () => {
18
+ const workerId = process.env.CLUSTER_WORKER_ID || 'unknown';
19
+ Logger.info(`Worker ${workerId} 收到 ${signal} 信号,正在关闭...`);
20
+
21
+ try {
22
+ // Bun Server 的 stop() 方法:关闭服务器
23
+ // 参数 true 表示强制关闭(不等待现有连接)
24
+ server.stop(true);
25
+ Logger.info(`Worker ${workerId} HTTP 服务器已关闭`);
26
+
27
+ // 给予短暂时间让资源清理完成
28
+ await Bun.sleep(100);
29
+
30
+ process.exit(0);
31
+ } catch (error) {
32
+ Logger.error(`Worker ${workerId} 关闭失败:`, error);
33
+ process.exit(1);
34
+ }
35
+ });
36
+ });
37
+
38
+ // 处理未捕获的异常,防止进程意外退出
39
+ process.on('uncaughtException', (error) => {
40
+ const workerId = process.env.CLUSTER_WORKER_ID || 'unknown';
41
+ Logger.error(`Worker ${workerId} 发生未捕获异常:`, error);
42
+ // 不退出进程,让 ClusterManager 决定是否重启
43
+ });
44
+
45
+ process.on('unhandledRejection', (reason) => {
46
+ const workerId = process.env.CLUSTER_WORKER_ID || 'unknown';
47
+ Logger.error(`Worker ${workerId} 发生未处理的 Promise 拒绝:`, reason);
48
+ // 不退出进程,让 ClusterManager 决定是否重启
49
+ });
@@ -6,6 +6,7 @@
6
6
  import { join } from 'pathe';
7
7
  import type { Subprocess } from 'bun';
8
8
  import { Logger } from '../lib/logger.js';
9
+ import { Befly } from '../main.js';
9
10
 
10
11
  export interface ClusterOptions {
11
12
  /** 实例数量(数字或 'max') */
@@ -16,8 +17,6 @@ export interface ClusterOptions {
16
17
  host: string;
17
18
  /** 项目根目录 */
18
19
  projectRoot: string;
19
- /** main.ts 文件路径 */
20
- mainFile: string;
21
20
  /** 环境变量 */
22
21
  env?: Record<string, string>;
23
22
  }
@@ -88,14 +87,14 @@ export class ClusterManager {
88
87
  * 启动单个 Worker
89
88
  */
90
89
  private spawnWorker(id: number, port: number): void {
91
- const { projectRoot, mainFile, host, env = {} } = this.options;
90
+ const { projectRoot, host, env = {} } = this.options;
92
91
 
93
92
  Logger.info(`启动 Worker ${id} (端口 ${port})...`);
94
93
 
95
- // 检查环境变量文件
96
- const envFile = join(projectRoot, '.env.production');
94
+ // 使用内置的 worker 入口文件
95
+ const workerFile = join(import.meta.dir, 'cluster-worker.ts');
97
96
 
98
- const proc = Bun.spawn(['bun', 'run', '--env-file=.env.production', mainFile], {
97
+ const proc = Bun.spawn(['bun', 'run', '--env-file=.env.production', workerFile], {
99
98
  cwd: projectRoot,
100
99
  stdout: 'inherit',
101
100
  stderr: 'inherit',
@@ -208,24 +207,38 @@ export class ClusterManager {
208
207
  // 向所有 Worker 发送 SIGTERM
209
208
  for (const [id, worker] of this.workers.entries()) {
210
209
  Logger.info(`关闭 Worker ${id} (端口 ${worker.port})...`);
211
- worker.process.kill('SIGTERM');
210
+ try {
211
+ worker.process.kill('SIGTERM');
212
+ } catch (error) {
213
+ Logger.warn(`无法向 Worker ${id} 发送 SIGTERM:`, error);
214
+ }
212
215
  }
213
216
 
214
- // 等待所有进程退出,最多 5
217
+ // 等待所有进程退出,最多 3 秒(Bun 的 server.stop() 很快)
215
218
  const timeout = setTimeout(() => {
216
- Logger.warn('等待超时,强制关闭所有 Worker');
219
+ Logger.warn('等待超时(3秒),强制关闭所有 Worker');
217
220
  for (const worker of this.workers.values()) {
218
- worker.process.kill('SIGKILL');
221
+ try {
222
+ worker.process.kill('SIGKILL');
223
+ } catch (error) {
224
+ // 忽略 SIGKILL 失败(进程可能已退出)
225
+ }
219
226
  }
227
+ // 强制退出主进程
228
+ setTimeout(() => process.exit(1), 500);
229
+ }, 3000);
230
+
231
+ try {
232
+ // 等待所有进程退出
233
+ await Promise.all(Array.from(this.workers.values()).map((w) => w.process.exited));
234
+ clearTimeout(timeout);
235
+ Logger.info('集群已安全关闭');
236
+ process.exit(0);
237
+ } catch (error) {
238
+ clearTimeout(timeout);
239
+ Logger.error('等待 Worker 退出时发生错误:', error);
220
240
  process.exit(1);
221
- }, 5000);
222
-
223
- // 等待所有进程退出
224
- await Promise.all(Array.from(this.workers.values()).map((w) => w.process.exited));
225
-
226
- clearTimeout(timeout);
227
- Logger.info('集群已安全关闭');
228
- process.exit(0);
241
+ }
229
242
  }
230
243
 
231
244
  /**
@@ -37,7 +37,7 @@ export class Lifecycle {
37
37
  * @param appContext - 应用上下文
38
38
  * @param callback - 启动完成后的回调函数
39
39
  */
40
- async start(appContext: BeflyContext, callback?: (server: Server) => void): Promise<void> {
40
+ async start(appContext: BeflyContext, callback?: (server: Server) => void): Promise<Server> {
41
41
  const serverStartTime = Bun.nanoseconds();
42
42
  Logger.info('开始启动 Befly 服务器...');
43
43
 
@@ -61,7 +61,7 @@ export class Lifecycle {
61
61
  const totalStartupTime = calcPerfTime(serverStartTime);
62
62
  Logger.info(`服务器启动准备完成,总耗时: ${totalStartupTime}`);
63
63
 
64
- await Bootstrap.start(
64
+ return await Bootstrap.start(
65
65
  {
66
66
  apiRoutes: this.apiRoutes,
67
67
  pluginLists: this.pluginLists,
@@ -7,7 +7,7 @@ import { relative, basename } from 'pathe';
7
7
  import { isPlainObject } from 'es-toolkit/compat';
8
8
  import { Logger } from '../lib/logger.js';
9
9
  import { calcPerfTime } from '../util.js';
10
- import { paths } from '../paths.js';
10
+ import { corePluginDir, projectPluginDir, coreApiDir, projectApiDir } from '../paths.js';
11
11
  import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
12
12
  import type { Plugin } from '../types/plugin.js';
13
13
  import type { ApiRoute } from '../types/api.js';
@@ -98,7 +98,7 @@ export class Loader {
98
98
  // 扫描核心插件目录
99
99
  const corePluginsScanStart = Bun.nanoseconds();
100
100
  for await (const file of glob.scan({
101
- cwd: paths.rootPluginDir,
101
+ cwd: corePluginDir,
102
102
  onlyFiles: true,
103
103
  absolute: true
104
104
  })) {
@@ -262,7 +262,7 @@ export class Loader {
262
262
  // 扫描用户插件目录
263
263
  const userPluginsScanStart = Bun.nanoseconds();
264
264
  for await (const file of glob.scan({
265
- cwd: paths.projectPluginDir,
265
+ cwd: projectPluginDir,
266
266
  onlyFiles: true,
267
267
  absolute: true
268
268
  })) {
@@ -393,11 +393,11 @@ export class Loader {
393
393
  let apiDir: string;
394
394
 
395
395
  if (where === 'core') {
396
- apiDir = paths.rootApiDir;
396
+ apiDir = coreApiDir;
397
397
  } else if (where === 'addon') {
398
398
  apiDir = getAddonDir(addonName, 'apis');
399
399
  } else {
400
- apiDir = paths.projectApiDir;
400
+ apiDir = projectApiDir;
401
401
  }
402
402
 
403
403
  let totalApis = 0;
package/main.ts CHANGED
@@ -32,8 +32,8 @@ export class Befly {
32
32
  * 启动服务器
33
33
  * @param callback - 启动完成后的回调函数
34
34
  */
35
- async listen(callback?: (server: Server) => void): Promise<void> {
36
- await this.lifecycle.start(this.appContext, callback);
35
+ async listen(callback?: (server: Server) => void): Promise<Server> {
36
+ return await this.lifecycle.start(this.appContext, callback);
37
37
  }
38
38
  }
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.3.5",
3
+ "version": "3.4.0",
4
4
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -80,5 +80,5 @@
80
80
  "ora": "^9.0.0",
81
81
  "pathe": "^2.0.3"
82
82
  },
83
- "gitHead": "c2f3a7f31ca73cf114fc206a799b78c68ec73c2b"
83
+ "gitHead": "2fbc9a9d7f4e6a30945c0bb274f3c5982949a8f6"
84
84
  }
package/paths.ts CHANGED
@@ -1,6 +1,31 @@
1
1
  /**
2
2
  * Befly 框架路径配置
3
- * 提供统一的路径变量,供整个框架使用
3
+ *
4
+ * 提供统一的路径常量,供整个框架使用
5
+ * 所有路径常量采用具名导出方式,避免通过对象访问
6
+ *
7
+ * 路径分类:
8
+ * - root* 系列:Core 框架内部路径(packages/core/*)
9
+ * - project* 系列:用户项目路径(process.cwd()/*)
10
+ *
11
+ * 目录结构:
12
+ * ```
13
+ * packages/core/ (coreDir)
14
+ * ├── scripts/ (coreScriptDir)
15
+ * ├── config/ (coreConfigDir)
16
+ * ├── checks/ (coreCheckDir)
17
+ * ├── plugins/ (corePluginDir)
18
+ * ├── apis/ (coreApiDir)
19
+ * └── tables/ (coreTableDir)
20
+ *
21
+ * project/ (projectDir)
22
+ * ├── scripts/ (projectScriptDir)
23
+ * ├── config/ (projectConfigDir)
24
+ * ├── checks/ (projectCheckDir)
25
+ * ├── plugins/ (projectPluginDir)
26
+ * ├── apis/ (projectApiDir)
27
+ * └── tables/ (projectTableDir)
28
+ * ```
4
29
  */
5
30
 
6
31
  import { fileURLToPath } from 'node:url';
@@ -13,22 +38,103 @@ const __dirname = dirname(__filename);
13
38
  // 项目根目录(befly 框架的使用方项目)
14
39
  const projectRoot = process.cwd();
15
40
 
41
+ // ==================== Core 框架路径 ====================
42
+
43
+ /**
44
+ * Core 框架根目录
45
+ * @description packages/core/
46
+ */
47
+ export const coreDir = __dirname;
48
+
49
+ /**
50
+ * Core 框架脚本目录
51
+ * @description packages/core/scripts/
52
+ * @usage 存放框架级别的脚本工具
53
+ */
54
+ export const coreScriptDir = join(__dirname, 'scripts');
55
+
56
+ /**
57
+ * Core 框架配置目录
58
+ * @description packages/core/config/
59
+ * @usage 存放框架默认配置(env.ts, fields.ts 等)
60
+ */
61
+ export const coreConfigDir = join(__dirname, 'config');
62
+
63
+ /**
64
+ * Core 框架检查目录
65
+ * @description packages/core/checks/
66
+ * @usage 存放启动检查模块(返回 boolean 的 default 函数)
67
+ */
68
+ export const coreCheckDir = join(__dirname, 'checks');
69
+
70
+ /**
71
+ * Core 框架插件目录
72
+ * @description packages/core/plugins/
73
+ * @usage 存放内置插件(db, logger, redis, tool 等)
74
+ */
75
+ export const corePluginDir = join(__dirname, 'plugins');
76
+
77
+ /**
78
+ * Core 框架 API 目录
79
+ * @description packages/core/apis/
80
+ * @usage 存放框架级别的 API 接口
81
+ */
82
+ export const coreApiDir = join(__dirname, 'apis');
83
+
84
+ /**
85
+ * Core 框架表定义目录
86
+ * @description packages/core/tables/
87
+ * @usage 存放框架核心表定义(JSON 格式)
88
+ */
89
+ export const coreTableDir = join(__dirname, 'tables');
90
+
91
+ // ==================== 用户项目路径 ====================
92
+
93
+ /**
94
+ * 项目根目录
95
+ * @description process.cwd()
96
+ * @usage 用户项目的根目录
97
+ */
98
+ export const projectDir = projectRoot;
99
+
100
+ /**
101
+ * 项目脚本目录
102
+ * @description {projectDir}/scripts/
103
+ * @usage 存放用户自定义脚本工具
104
+ */
105
+ export const projectScriptDir = join(projectRoot, 'scripts');
106
+
107
+ /**
108
+ * 项目配置目录
109
+ * @description {projectDir}/config/
110
+ * @usage 存放用户项目配置(覆盖框架默认配置)
111
+ */
112
+ export const projectConfigDir = join(projectRoot, 'config');
113
+
16
114
  /**
17
- * 系统路径配置对象
18
- */
19
- export const paths = {
20
- rootDir: __dirname,
21
- rootScriptDir: join(__dirname, 'scripts'),
22
- rootConfigDir: join(__dirname, 'config'),
23
- rootCheckDir: join(__dirname, 'checks'),
24
- rootPluginDir: join(__dirname, 'plugins'),
25
- rootApiDir: join(__dirname, 'apis'),
26
- coreTableDir: join(__dirname, 'tables'),
27
- projectDir: projectRoot,
28
- projectScriptDir: join(projectRoot, 'scripts'),
29
- projectConfigDir: join(projectRoot, 'config'),
30
- projectCheckDir: join(projectRoot, 'checks'),
31
- projectPluginDir: join(projectRoot, 'plugins'),
32
- projectApiDir: join(projectRoot, 'apis'),
33
- projectTableDir: join(projectRoot, 'tables')
34
- } as const;
115
+ * 项目检查目录
116
+ * @description {projectDir}/checks/
117
+ * @usage 存放用户自定义启动检查模块
118
+ */
119
+ export const projectCheckDir = join(projectRoot, 'checks');
120
+
121
+ /**
122
+ * 项目插件目录
123
+ * @description {projectDir}/plugins/
124
+ * @usage 存放用户自定义插件
125
+ */
126
+ export const projectPluginDir = join(projectRoot, 'plugins');
127
+
128
+ /**
129
+ * 项目 API 目录
130
+ * @description {projectDir}/apis/
131
+ * @usage 存放用户业务 API 接口
132
+ */
133
+ export const projectApiDir = join(projectRoot, 'apis');
134
+
135
+ /**
136
+ * 项目表定义目录
137
+ * @description {projectDir}/tables/
138
+ * @usage 存放用户业务表定义(JSON 格式)
139
+ */
140
+ export const projectTableDir = join(projectRoot, 'tables');
package/router/static.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { join } from 'pathe';
7
- import { paths } from '../paths.js';
7
+ import { projectDir } from '../paths.js';
8
8
  import { No } from '../util.js';
9
9
  import { setCorsOptions } from '../lib/middleware.js';
10
10
  import { Logger } from '../lib/logger.js';
@@ -16,7 +16,7 @@ import { Env } from '../config/env.js';
16
16
  export async function staticHandler(req: Request): Promise<Response> {
17
17
  const corsOptions = setCorsOptions(req);
18
18
  const url = new URL(req.url);
19
- const filePath = join(paths.projectDir, 'public', url.pathname);
19
+ const filePath = join(projectDir, 'public', url.pathname);
20
20
 
21
21
  try {
22
22
  // OPTIONS预检请求
package/util.ts CHANGED
@@ -22,7 +22,7 @@ import { isEmpty, isPlainObject } from 'es-toolkit/compat';
22
22
  import { snakeCase, camelCase, kebabCase } from 'es-toolkit/string';
23
23
  import { Env } from './config/env.js';
24
24
  import { Logger } from './lib/logger.js';
25
- import { paths } from './paths.js';
25
+ import { projectDir } from './paths.js';
26
26
  import type { KeyValue } from './types/common.js';
27
27
  import type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from './types/jwt';
28
28
  import type { Plugin } from './types/plugin.js';
@@ -241,7 +241,7 @@ export const parseRule = (rule: string): ParsedFieldRule => {
241
241
  * 扫描所有可用的 addon
242
242
  */
243
243
  export const scanAddons = (): string[] => {
244
- const beflyDir = join(paths.projectDir, 'node_modules', '@befly-addon');
244
+ const beflyDir = join(projectDir, 'node_modules', '@befly-addon');
245
245
 
246
246
  if (!existsSync(beflyDir)) {
247
247
  return [];
@@ -270,7 +270,7 @@ export const scanAddons = (): string[] => {
270
270
  * 获取 addon 的指定子目录路径
271
271
  */
272
272
  export const getAddonDir = (addonName: string, subDir: string): string => {
273
- return join(paths.projectDir, 'node_modules', '@befly-addon', addonName, subDir);
273
+ return join(projectDir, 'node_modules', '@befly-addon', addonName, subDir);
274
274
  };
275
275
 
276
276
  /**
package/config/menu.json DELETED
@@ -1,67 +0,0 @@
1
- [
2
- {
3
- "name": "首页",
4
- "path": "/",
5
- "icon": "Home",
6
- "sort": 1,
7
- "type": 1
8
- },
9
- {
10
- "name": "管理员管理",
11
- "path": "/admin",
12
- "icon": "Users",
13
- "sort": 2,
14
- "type": 1
15
- },
16
- {
17
- "name": "新闻管理",
18
- "path": "/news",
19
- "icon": "Newspaper",
20
- "sort": 3,
21
- "type": 1
22
- },
23
- {
24
- "name": "菜单管理",
25
- "path": "/menu",
26
- "icon": "Menu",
27
- "sort": 4,
28
- "type": 1
29
- },
30
- {
31
- "name": "角色管理",
32
- "path": "/role",
33
- "icon": "Users",
34
- "sort": 5,
35
- "type": 1
36
- },
37
- {
38
- "name": "字典管理",
39
- "path": "/dict",
40
- "icon": "BookOpen",
41
- "sort": 6,
42
- "type": 1
43
- },
44
- {
45
- "name": "用户管理",
46
- "path": "/user",
47
- "icon": "UserCog",
48
- "sort": 7,
49
- "type": 1,
50
- "children": [
51
- {
52
- "name": "用户列表",
53
- "path": "/list",
54
- "icon": "Minus",
55
- "sort": 1,
56
- "type": 1
57
- },
58
- {
59
- "name": "用户权限",
60
- "path": "/permission",
61
- "icon": "Minus",
62
- "sort": 2,
63
- "type": 1
64
- }
65
- ]
66
- }
67
- ]