@mindbase/express-common 1.0.7 → 1.0.9

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.
@@ -0,0 +1,62 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { pathToFileURL } from "url";
4
+ import { ScanResult } from "../feature/scanner/FileScanner";
5
+
6
+ /**
7
+ * 生成 .drizzle-schemas.json 文件
8
+ * @param scannedResults 扫描结果
9
+ * @param cwd 当前工作目录
10
+ */
11
+ export function generateDrizzleSchemasJson(
12
+ scannedResults: ScanResult[],
13
+ cwd: string = process.cwd()
14
+ ): void {
15
+ // 过滤 ORM schema(路径包含 /orm/)
16
+ const ormSchemaFiles = scannedResults
17
+ .filter((r) => r.type === "schema" && r.filePath.includes("/orm/"))
18
+ .map((f) => path.relative(cwd, f.filePath).replace(/\\/g, "/"));
19
+
20
+ // 生成到当前工作目录(宿主项目根目录)
21
+ const schemaJsonPath = path.join(cwd, ".drizzle-schemas.json");
22
+ fs.writeFileSync(schemaJsonPath, JSON.stringify(ormSchemaFiles, null, 2));
23
+
24
+ console.log(`📄 准备模式:生成 schema 清单...`);
25
+ console.log(`✅ 已生成 ${ormSchemaFiles.length} 个 schema 文件路径`);
26
+ console.log(`📁 文件位置: ${schemaJsonPath}`);
27
+ }
28
+
29
+ /**
30
+ * 准备命令 - 通过环境变量控制复用宿主 app.ts
31
+ */
32
+ export async function prepare(cwd: string = process.cwd()): Promise<void> {
33
+ console.log("🚀 MindBase 环境准备工具");
34
+ console.log(`工作目录: ${cwd}\n`);
35
+
36
+ const appPath = path.join(cwd, "src/app.ts");
37
+
38
+ if (!fs.existsSync(appPath)) {
39
+ console.error("❌ 未找到 src/app.ts");
40
+ process.exit(1);
41
+ }
42
+
43
+ try {
44
+ console.log("📄 加载宿主应用...");
45
+
46
+ // 设置准备模式环境变量
47
+ process.env.PREPARE_MODE = "true";
48
+
49
+ // 动态导入并执行宿主 app.ts
50
+ await import(pathToFileURL(appPath).href);
51
+
52
+ console.log("\n✅ 环境准备完成!");
53
+ } catch (error) {
54
+ console.error("\n❌ 准备失败:", error);
55
+ process.exit(1);
56
+ }
57
+ }
58
+
59
+ // 兼容旧代码:保留 precache 别名
60
+ export async function precache(cwd?: string): Promise<void> {
61
+ return prepare(cwd);
62
+ }
package/core/app.ts ADDED
@@ -0,0 +1,186 @@
1
+ import { Express } from "express";
2
+ import { createState } from "./state";
3
+ import { setBaseDir, scan, addScanPath } from "../feature/scanner/FileScanner";
4
+ import { MindBaseAppOptions } from "../types/Index";
5
+ import initExpress from "../utils/InitExpress";
6
+ import { setupDatabase } from "../utils/InitDatabase";
7
+ import { registerComponents } from "../utils/ComponentRegistry";
8
+ import { setupErrorHandlers } from "../utils/InitErrorHandler";
9
+ import { startServer } from "../utils/HttpServer";
10
+ import logger from "../utils/Logger";
11
+ import { initDocManager } from "../utils/DocManager";
12
+ import docRouter from "../routes/Doc.route";
13
+ import { generateDrizzleSchemasJson } from "../commands/prepare";
14
+
15
+ /**
16
+ * MindBase 应用实例
17
+ */
18
+ export interface MindBaseApp {
19
+ /** 注册插件模块 */
20
+ use(plugin: any): void;
21
+ /** 启动应用(初始化数据库、注册路由、启动 HTTP 服务) */
22
+ startup(): Promise<void>;
23
+ /** 获取 Express 实例 */
24
+ getApp(): Express;
25
+ /** 获取 Drizzle 数据库实例 */
26
+ getDB(): any;
27
+ /** 获取应用配置选项 */
28
+ getOptions(): MindBaseAppOptions;
29
+ }
30
+
31
+ /**
32
+ * 创建 MindBase 应用实例
33
+ * @param options 应用配置选项
34
+ * @returns MindBase 应用实例
35
+ * @example
36
+ * const app = createApp({
37
+ * port: 3000,
38
+ * logLevel: "debug",
39
+ * });
40
+ * app.use(auth);
41
+ * await app.startup();
42
+ */
43
+ export function createApp(options: MindBaseAppOptions = {}): MindBaseApp {
44
+ logger.startup("初始化", "应用初始化……");
45
+ const stateInstance = createState(options);
46
+ const plugins: any[] = [];
47
+
48
+ // 初始化日志级别
49
+ if (stateInstance.options.logLevel) {
50
+ logger.setLevel(stateInstance.options.logLevel);
51
+ }
52
+
53
+ const app: MindBaseApp = {
54
+ use: async (plugin) => {
55
+ if (typeof plugin.install === "function") {
56
+ plugins.push(plugin);
57
+ }
58
+ if (typeof plugin.__modulePath === "string") {
59
+ logger.startup("模块", `注册:${plugin.__modulePath}`);
60
+ addScanPath(plugin.__modulePath);
61
+ }
62
+ },
63
+ startup: async () => {
64
+ const isPrepareMode = process.env.PREPARE_MODE === "true";
65
+ // 0. 初始化 Express 基础配置
66
+ if (!isPrepareMode) {
67
+ initExpress(stateInstance);
68
+ }
69
+
70
+ // 1. 准备扫描环境
71
+ await prepareScanEnvironment();
72
+
73
+ // 2. 扫描组件
74
+ const scannedResults = await scanComponents();
75
+
76
+ // 准备模式:生成 schema 清单后返回,不启动服务
77
+ if (isPrepareMode) {
78
+ generateDrizzleSchemasJson(scannedResults, process.cwd());
79
+ return;
80
+ }
81
+
82
+ // 3. 初始化数据库
83
+ await initializeDataStorage(scannedResults);
84
+
85
+ // 4. 初始化模块
86
+ await installPlugins();
87
+
88
+ // 5. 注册组件
89
+
90
+ registerComponents(stateInstance, scannedResults);
91
+
92
+ // 6. 注册文档路由
93
+ registerDocRoutes();
94
+
95
+ // 7. 注册错误处理
96
+ setupErrorHandlers(stateInstance.express, stateInstance.options.logging);
97
+
98
+ // 8. 启动 HTTP 服务
99
+ await startHttpServer();
100
+
101
+ logger.startup("初始化", "✅ 应用启动完成");
102
+ },
103
+
104
+ getApp: () => stateInstance.express,
105
+ getDB: () => stateInstance.db,
106
+ getOptions: () => stateInstance.options,
107
+ };
108
+
109
+ // 准备扫描环境
110
+ async function prepareScanEnvironment() {
111
+ setBaseDir(process.cwd());
112
+ logger.startup("扫描", `基准目录: ${process.cwd()}`);
113
+ }
114
+
115
+ // 扫描组件
116
+ async function scanComponents() {
117
+ const scannedResults = await scan();
118
+
119
+ // 统计缓存命中情况
120
+ const routeFiles = scannedResults.filter((r) => r.type === "route");
121
+ const cacheHits = routeFiles.filter((r) => r.cacheHit).length;
122
+ const total = routeFiles.length;
123
+
124
+ if (cacheHits > 0) {
125
+ logger.startup("扫描", `发现 ${scannedResults.length} 个组件文件 (缓存命中: ${cacheHits}/${total})`);
126
+ } else {
127
+ logger.startup("扫描", `发现 ${scannedResults.length} 个组件文件`);
128
+ }
129
+
130
+ return scannedResults;
131
+ }
132
+
133
+ // 初始化数据存储
134
+ async function initializeDataStorage(scannedResults) {
135
+ // 初始化数据库与 Schema
136
+ setupDatabase(stateInstance, scannedResults);
137
+ stateInstance.express.set("db", stateInstance.db);
138
+
139
+ // 收集数据库信息
140
+ if (stateInstance.db) {
141
+ const dbPath = stateInstance.options.database?.path || "./data/app.db";
142
+ logger.startup("数据库", `已连接: SQLite (${dbPath})`);
143
+ }
144
+
145
+ // 初始化文档管理器
146
+ const docDbPath = stateInstance.options.database?.path
147
+ ? stateInstance.options.database.path.replace(/app\.db$/, "doc.db")
148
+ : "./data/doc.db";
149
+ initDocManager({ path: docDbPath });
150
+ logger.startup("数据库", `文档库已初始化: ${docDbPath}`);
151
+ }
152
+
153
+ // 执行插件安装
154
+ async function installPlugins() {
155
+ for (const plugin of plugins) {
156
+ await plugin.install(app);
157
+ }
158
+ }
159
+
160
+ // 启动 HTTP 服务
161
+ async function startHttpServer() {
162
+ const listenPort = stateInstance.options.port || 3000;
163
+ await startServer(stateInstance.express, listenPort);
164
+
165
+ // 打印服务器启动信息
166
+ const serverUrl = `http://${stateInstance.options.host}:${listenPort}`;
167
+ logger.startup("服务", `访问地址: ${serverUrl}`);
168
+ }
169
+
170
+ // 注册文档路由
171
+ function registerDocRoutes() {
172
+ const apiPrefix = stateInstance.options.apiPrefix || "/api";
173
+ const docRoutePath = `${apiPrefix}/doc`.replace(/\/+/g, "/");
174
+ stateInstance.express.use(docRoutePath, docRouter);
175
+ stateInstance.options.authWhitelist.push(`${docRoutePath}/modules`);
176
+ logger.startup("路由", `${docRoutePath}/modules (GET)`);
177
+ stateInstance.options.authWhitelist.push(`${docRoutePath}/routes`);
178
+ logger.startup("路由", `${docRoutePath}/routes (GET)`);
179
+ stateInstance.options.authWhitelist.push(new RegExp(`^${docRoutePath}/routes/[^/]+$`));
180
+ logger.startup("路由", `${docRoutePath}/routes/:id (GET)`);
181
+ stateInstance.options.authWhitelist.push(`${docRoutePath}/sync`);
182
+ logger.startup("路由", `${docRoutePath}/sync (POST)`);
183
+ }
184
+
185
+ return app;
186
+ }
@@ -0,0 +1,38 @@
1
+ import { findPackageRoot } from "./FindPackageRoot";
2
+ import { getModulePath } from "./GetModulePath";
3
+ import { MindBaseApp } from "../../types";
4
+ import logger from "../../utils/Logger";
5
+
6
+ export interface LightUpModule {
7
+ install(app: MindBaseApp): Promise<void>;
8
+ __modulePath: string;
9
+ }
10
+
11
+ /**
12
+ * 创建一个 LightUp 模块
13
+ * @param install 模块安装函数
14
+ * @param callerPath 调用者的文件路径(通常是 __filename),如果不提供则尝试通过调用栈获取
15
+ * @returns LightUp 模块实例
16
+ * @throws 如果模块创建失败
17
+ */
18
+ export function createModule(install, callerPath?: string): LightUpModule {
19
+ if (typeof install !== "function") {
20
+ throw new Error("模块安装函数必须是一个函数");
21
+ }
22
+
23
+ try {
24
+ // 优先使用传入的 callerPath,否则通过调用栈获取
25
+ const modulePath = callerPath || getModulePath();
26
+ const packageRoot = findPackageRoot(modulePath);
27
+
28
+ logger.debug(`创建模块:路径=${modulePath},包根目录=${packageRoot}`);
29
+
30
+ return {
31
+ install,
32
+ __modulePath: packageRoot,
33
+ };
34
+ } catch (error) {
35
+ logger.error("模块创建失败:", error);
36
+ throw new Error(`模块创建失败:${error instanceof Error ? error.message : String(error)}`);
37
+ }
38
+ }
@@ -0,0 +1,58 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+
4
+ /**
5
+ * 查找包含 package.json 的根目录
6
+ * @param startPath 开始查找的路径
7
+ * @returns 包含 package.json 的目录路径
8
+ * @throws 如果输入路径无效
9
+ */
10
+ export function findPackageRoot(startPath: string): string {
11
+ // 验证输入路径
12
+ if (!startPath || typeof startPath !== "string") {
13
+ throw new Error("无效的起始路径:路径必须是非空字符串");
14
+ }
15
+
16
+ // 确保路径是绝对路径
17
+ let currentPath: string;
18
+ try {
19
+ currentPath = path.isAbsolute(startPath) ? startPath : path.resolve(startPath);
20
+ } catch (e) {
21
+ throw new Error(`无法解析路径:${startPath}`);
22
+ }
23
+
24
+ // 验证路径是否存在
25
+ try {
26
+ if (!fs.existsSync(currentPath)) {
27
+ throw new Error(`路径不存在:${currentPath}`);
28
+ }
29
+
30
+ // 如果是文件,从其父目录开始查找
31
+ const stat = fs.statSync(currentPath);
32
+ if (stat.isFile()) {
33
+ currentPath = path.dirname(currentPath);
34
+ }
35
+ } catch (e) {
36
+ throw new Error(`路径验证失败:${e instanceof Error ? e.message : String(e)}`);
37
+ }
38
+
39
+ // 向上查找 package.json
40
+ while (currentPath !== path.parse(currentPath).root) {
41
+ try {
42
+ const pkgJsonPath = path.join(currentPath, "package.json");
43
+ if (fs.existsSync(pkgJsonPath)) {
44
+ // 验证找到的 package.json 是否是文件
45
+ const pkgStat = fs.statSync(pkgJsonPath);
46
+ if (pkgStat.isFile()) {
47
+ return currentPath;
48
+ }
49
+ }
50
+ } catch (e) {
51
+ // 忽略单个路径的访问错误,继续向上查找
52
+ }
53
+ currentPath = path.dirname(currentPath);
54
+ }
55
+
56
+ // 如果没有找到 package.json,返回起始路径的父目录
57
+ return path.dirname(startPath);
58
+ }
@@ -0,0 +1,58 @@
1
+ import * as path from "path";
2
+
3
+ /**
4
+ * 获取当前模块的路径
5
+ * @returns 当前模块的绝对路径
6
+ * @throws 如果无法确定模块路径
7
+ */
8
+ export function getModulePath(): string {
9
+ // 从调用栈中提取调用者的路径
10
+ try {
11
+ const error = new Error();
12
+ if (error.stack) {
13
+ console.log("DEBUG STACK:", error.stack);
14
+ const stackLines = error.stack.split("\n");
15
+
16
+ // 跳过内部调用,找到真正的调用者
17
+ // 需要跳过:getModulePath, createModule, ts-node, node:internal
18
+ for (const line of stackLines) {
19
+ // 跳过内部函数和 Node.js/ts-node 的调用
20
+ if (!line.includes("at") ||
21
+ line.includes("getModulePath") ||
22
+ line.includes("createModule") ||
23
+ line.includes("CreateModule.ts") ||
24
+ line.includes("ts-node") ||
25
+ line.includes("node:internal")) {
26
+ continue;
27
+ }
28
+
29
+ // 提取文件路径
30
+ // 格式可能是:at xxx (/path/to/file.ts:1:2) 或 at /path/to/file.ts:1:2
31
+ const match = line.match(/\(([^:]+\.ts)/) || line.match(/at\s+([^:]+\.ts)/);
32
+ if (match && match[1]) {
33
+ const filePath = match[1];
34
+ console.log("提取到的路径:", filePath);
35
+ return path.resolve(filePath);
36
+ }
37
+ }
38
+ }
39
+ } catch (e) {
40
+ // 忽略提取 stack 时的错误
41
+ }
42
+
43
+ throw new Error("无法确定模块路径:当前环境不支持 __filename 且无法从调用栈中提取路径");
44
+ }
45
+
46
+ /**
47
+ * 获取当前模块所在的目录
48
+ * @returns 当前模块的目录路径
49
+ * @throws 如果无法确定模块路径
50
+ */
51
+ export function getModuleDir(): string {
52
+ try {
53
+ const modulePath = getModulePath();
54
+ return path.dirname(modulePath);
55
+ } catch (error) {
56
+ throw new Error(`无法获取模块目录:${error instanceof Error ? error.message : String(error)}`);
57
+ }
58
+ }
package/core/state.ts ADDED
@@ -0,0 +1,67 @@
1
+ import express, { Express } from "express";
2
+ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
3
+ import { LogLevel } from "../types";
4
+
5
+ export interface MindBaseAppOptions {
6
+ /** 服务监听主机名 @default 127.0.0.1 */
7
+ host?: string;
8
+ /** 服务监听端口 @default 3000 */
9
+ port?: number;
10
+ /** 是否启用请求日志 @default true */
11
+ logging?: boolean;
12
+ /** 静态文件目录路径 */
13
+ staticPath?: string;
14
+ /** 是否解析 User-Agent 信息 @default true */
15
+ userAgent?: boolean;
16
+ /** 是否解析 IP 地理位置信息 @default true */
17
+ ip?: boolean;
18
+ /** 是否启用 CORS 跨域 @default true */
19
+ cors?: boolean;
20
+ /** API 路由前缀,如 "/api" */
21
+ apiPrefix?: string;
22
+ /**
23
+ * 日志级别(优先级从低到高):
24
+ * - debug: 调试信息(显示所有)
25
+ * - info: 普通信息(隐藏 debug)
26
+ * - warn: 警告信息(隐藏 debug、info)
27
+ * - error: 错误信息(隐藏 debug、info、warn)
28
+ * - silent: 静默模式(隐藏所有)
29
+ * @default "info"
30
+ */
31
+ logLevel?: LogLevel;
32
+ /** 数据库配置 */
33
+ database?: {
34
+ /** SQLite 数据库文件路径 @default "./data/app.db" */
35
+ path?: string;
36
+ };
37
+ /** 认证白名单路径(支持字符串和正则表达式) */
38
+ authWhitelist?: (string | RegExp)[];
39
+ }
40
+ export interface AppState {
41
+ options: MindBaseAppOptions;
42
+ express: Express;
43
+ db?: BetterSQLite3Database<Record<string, any>>; // Drizzle 数据库实例
44
+ schemas?: Record<string, any>; // 合并后的所有 schema
45
+ }
46
+
47
+ export function createState(options: MindBaseAppOptions = {}): AppState {
48
+ return {
49
+ options: {
50
+ host: options.host || "127.0.0.1",
51
+ port: options.port || 3000,
52
+ logging: options.logging !== false,
53
+ staticPath: options.staticPath,
54
+ userAgent: options.userAgent !== false,
55
+ ip: options.ip !== false,
56
+ cors: options.cors !== false,
57
+ apiPrefix: options.apiPrefix,
58
+ logLevel: options.logLevel || "info",
59
+ authWhitelist: [],
60
+ database: {
61
+ path: options.database?.path || "./data/app.db",
62
+ },
63
+ },
64
+ express: express(),
65
+ schemas: {},
66
+ };
67
+ }
@@ -0,0 +1,63 @@
1
+ import { CronJob } from "cron";
2
+ import { randomUUID } from "crypto";
3
+
4
+ interface CronConfig {
5
+ timezone?: string;
6
+ }
7
+
8
+ interface CronJobMap {
9
+ [id: string]: CronJob;
10
+ }
11
+
12
+ let jobs: CronJobMap = {};
13
+ let config: CronConfig = {
14
+ timezone: "Asia/Shanghai",
15
+ };
16
+
17
+ export function setConfig(newConfig: CronConfig): void {
18
+ config = {
19
+ ...config,
20
+ ...newConfig,
21
+ };
22
+ }
23
+
24
+ export function addCron(pattern: string, handler: () => void | Promise<void>, customId?: string): string {
25
+ const id = customId || generateId();
26
+ if (jobs[id]) {
27
+ jobs[id].stop();
28
+ }
29
+ const job = new CronJob(pattern, handler, null, true, config.timezone);
30
+ jobs[id] = job;
31
+ return id;
32
+ }
33
+
34
+ export function stopCron(id: string): boolean {
35
+ const job = jobs[id];
36
+ if (job) {
37
+ job.stop();
38
+ delete jobs[id];
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ export function stopAllCron(): void {
45
+ Object.values(jobs).forEach((job) => {
46
+ job.stop();
47
+ });
48
+ jobs = {};
49
+ }
50
+
51
+ function generateId(): string {
52
+ return randomUUID();
53
+ }
54
+
55
+ const CronManager = {
56
+ setConfig,
57
+ addCron,
58
+ stopCron,
59
+ stopAllCron,
60
+ };
61
+
62
+ export default CronManager;
63
+ export type { CronConfig };