diff --git a/lib/file-handle.d.ts b/lib/file-handle.d.ts index 8c3c1bf..c3136cd 100644 --- a/lib/file-handle.d.ts +++ b/lib/file-handle.d.ts @@ -37,7 +37,7 @@ export declare function writeFile(path: string | PathLike, data: any): void; export declare function readFile(path: string | PathLike, options?: { encoding?: null | undefined; flag?: string | undefined; -} | null): Buffer | undefined; +} | null): Buffer | undefined; /** * @name 拷贝文件夹 * @export diff --git a/lib/server/polyfill.d.ts b/lib/server/polyfill.d.ts index 46d82ec..23a636a 100644 --- a/lib/server/polyfill.d.ts +++ b/lib/server/polyfill.d.ts @@ -1,3 +1,11 @@ export type GenerateIdOption = { shuffle?: boolean; }; +export type LogFormatterProp = { + level: "info" | "warn" | "error"; + caller: NodeJS.CallSite | null; + args: any[]; +}; +export type LogFormatter = (props: LogFormatterProp) => any[]; +export declare const polyfillConsole: (id: string, trace: boolean, formatter?: LogFormatter) => void; +export declare const removePolyfill: (id: string) => void; diff --git a/lib/server/polyfill.js b/lib/server/polyfill.js index d94e579..91db88e 100644 --- a/lib/server/polyfill.js +++ b/lib/server/polyfill.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.removePolyfill = exports.polyfillConsole = void 0; const uuid_1 = require("uuid"); async function generateNewId(option) { if (option?.shuffle && process.env.NODE_ENV === 'development') { @@ -10,3 +11,97 @@ async function generateNewId(option) { Object.assign(global, { generateNewId, }); +// 存储所有的 polyfill 配置栈 +const polyfillStack = []; +const originalConsoleMethods = {}; +// 获取调用堆栈信息的工厂函数 +const createGetCallerInfo = (trace) => () => { + if (!trace) { + return null; + } + const originalFunc = Error.prepareStackTrace; + let callerInfo = null; + try { + const err = new Error(); + Error.prepareStackTrace = (err, stack) => stack; + const stack = err.stack; + const currentFile = stack[0].getFileName(); + for (let i = 1; i < stack.length; i++) { + const callSite = stack[i]; + if (currentFile !== callSite.getFileName()) { + callerInfo = callSite; + break; + } + } + } + catch (e) { + console.error(e); + } + Error.prepareStackTrace = originalFunc; + return callerInfo; +}; +const polyfillConsole = (id, trace, formatter) => { + // 检查是否已经添加过相同 id 的 polyfill + if (polyfillStack.some(item => item.id === id)) { + return; + } + // 第一次调用时保存原始方法 + if (polyfillStack.length === 0) { + originalConsoleMethods.info = console.info; + originalConsoleMethods.warn = console.warn; + originalConsoleMethods.error = console.error; + } + // 添加新的 polyfill 到栈顶 + polyfillStack.push({ id, trace, formatter }); + // 重新设置 console 方法 + ["info", "warn", "error"].forEach((level) => { + const levelStr = level; + const originalFunc = originalConsoleMethods[levelStr]; + console[levelStr] = function (...args) { + let processedArgs = args; + // 从后往前执行所有 formatter + for (let i = polyfillStack.length - 1; i >= 0; i--) { + const item = polyfillStack[i]; + if (item.formatter) { + const getCallerInfo = createGetCallerInfo(item.trace); + processedArgs = item.formatter({ + level: levelStr, + caller: getCallerInfo(), + args: processedArgs, + }); + } + } + originalFunc(...processedArgs); + }; + }); + console.info(`new console polyfill added: ${id}`); +}; +exports.polyfillConsole = polyfillConsole; +// 可选:提供移除 polyfill 的功能 +const removePolyfill = (id) => { + const index = polyfillStack.findIndex(item => item.id === id); + if (index === -1) { + return; + } + polyfillStack.splice(index, 1); + // 如果栈为空,恢复原始方法 + if (polyfillStack.length === 0) { + console.info = originalConsoleMethods.info; + console.warn = originalConsoleMethods.warn; + console.error = originalConsoleMethods.error; + } + else { + // 否则重新应用所有 polyfill + const tempStack = [...polyfillStack]; + polyfillStack.length = 0; + Object.keys(originalConsoleMethods).forEach(level => { + const levelStr = level; + console[levelStr] = originalConsoleMethods[levelStr]; + }); + tempStack.forEach(item => { + polyfillStack.length = 0; // 清空以便重新添加 + (0, exports.polyfillConsole)(item.id, item.trace, item.formatter); + }); + } +}; +exports.removePolyfill = removePolyfill; diff --git a/lib/server/start.js b/lib/server/start.js index b55f511..b99c28f 100644 --- a/lib/server/start.js +++ b/lib/server/start.js @@ -23,6 +23,7 @@ const koa_mount_1 = tslib_1.__importDefault(require("koa-mount")); const chalk_1 = tslib_1.__importDefault(require("chalk")); const utils_1 = require("../utils"); const bcryptjs_1 = tslib_1.__importDefault(require("bcryptjs")); +const polyfill_1 = require("./polyfill"); (0, utils_1.checkNodeVersion)(); const socketAdminUI = (0, path_1.join)(__dirname, '../../ui/socket-admin'); const DATA_SUBSCRIBE_NAMESPACE = '/dsn'; @@ -38,6 +39,7 @@ function concat(...paths) { }); } async function startup(path, connector, omitWatchers, omitTimers, routine) { + const errorHandler = require((0, path_1.join)(path, 'lib', 'configuration', 'errors')).default; const serverConfiguration = require((0, path_1.join)(path, 'lib', 'configuration', 'server')).default; // 拿到package.json,用作项目的唯一标识,否则无法区分不同项目的Redis+socketIO连接 const packageJson = require((0, path_1.join)(path, 'package.json')); @@ -164,6 +166,16 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) { await appLoader.unmount(); return result; } + if (errorHandler && typeof errorHandler === 'function') { + (0, polyfill_1.polyfillConsole)("startup", true, (props) => { + if (props.level === "error") { + appLoader.execRoutine(async (ctx) => { + await errorHandler(props.caller, props.args, ctx); + }); + } + return props.args; + }); + } // 否则启动服务器模式 koa.use(async (ctx, next) => { try { @@ -365,6 +377,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) { await httpServer.close(); await koa.removeAllListeners(); await appLoader.unmount(); + (0, polyfill_1.removePolyfill)("startup"); }; return shutdown; } diff --git a/lib/server/watch.d.ts b/lib/server/watch.d.ts index 632ca93..355a8e2 100644 --- a/lib/server/watch.d.ts +++ b/lib/server/watch.d.ts @@ -1,9 +1,4 @@ -export type LogFormatterProp = { - level: "info" | "warn" | "error"; - caller: NodeJS.CallSite | null; - args: any[]; -}; -export type LogFormatter = (props: LogFormatterProp) => any[]; +import { LogFormatter } from "./polyfill"; export type WatchConfig = { autoUpdateI18n?: boolean; /** diff --git a/lib/server/watch.js b/lib/server/watch.js index 62883cb..d93019a 100644 --- a/lib/server/watch.js +++ b/lib/server/watch.js @@ -8,6 +8,7 @@ const path_1 = tslib_1.__importDefault(require("path")); const dayjs_1 = tslib_1.__importDefault(require("dayjs")); const fs_1 = tslib_1.__importDefault(require("fs")); const lodash_1 = require("lodash"); +const polyfill_1 = require("./polyfill"); // 创建事件发射器 const createEventEmitter = () => { const listeners = new Map(); @@ -580,50 +581,9 @@ const watch = (projectPath, config) => { } }; }; - const polyfillConsole = (trace) => { - // 获取调用堆栈信息 - const getCallerInfo = () => { - if (!trace) { - return null; - } - const originalFunc = Error.prepareStackTrace; - let callerInfo = null; - try { - const err = new Error(); - Error.prepareStackTrace = (err, stack) => stack; - const stack = err.stack; // Type assertion here - const currentFile = stack[0].getFileName(); - for (let i = 1; i < stack.length; i++) { - // Start from index 1 - const callSite = stack[i]; - if (currentFile !== callSite.getFileName()) { - callerInfo = callSite; - break; - } - } - } - catch (e) { - console.error(e); - } - Error.prepareStackTrace = originalFunc; - return callerInfo; - }; - // polyfill console.log 添加时间和文件位置 - ["info", "warn", "error"].forEach((level) => { - const levelStr = level; - const oldFunc = console[levelStr]; - console[levelStr] = function (...args) { - oldFunc(...(realConfig.polyfill?.console?.formatter({ - level: levelStr, - caller: getCallerInfo(), - args, - }) || [])); - }; - }); - }; // 初始化polyfill polyfillLoader(); - realConfig.polyfill.console.enable && polyfillConsole(enableTrace); + realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter); // 初始编译检查 const initialCompile = () => { const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js"); diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts new file mode 100644 index 0000000..6ae2aa9 --- /dev/null +++ b/lib/types/index.d.ts @@ -0,0 +1,4 @@ +import { EntityDict } from "oak-domain/lib/types"; +import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; +import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; +export type ErrorHandler = (caller: NodeJS.CallSite | null, args: any[], ctx: AsyncContext) => Promise; diff --git a/lib/types/index.js b/lib/types/index.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/lib/types/index.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/server/polyfill.ts b/src/server/polyfill.ts index 2e589e8..dce309b 100644 --- a/src/server/polyfill.ts +++ b/src/server/polyfill.ts @@ -14,3 +14,125 @@ async function generateNewId(option?: GenerateIdOption) { Object.assign(global, { generateNewId, }); + +export type LogFormatterProp = { + level: "info" | "warn" | "error"; + caller: NodeJS.CallSite | null; + args: any[]; +}; + +export type LogFormatter = (props: LogFormatterProp) => any[]; + +type PolyfillItem = { + id: string; + trace: boolean; + formatter?: LogFormatter; +}; + +// 存储所有的 polyfill 配置栈 +const polyfillStack: PolyfillItem[] = []; +const originalConsoleMethods: { + info?: typeof console.info; + warn?: typeof console.warn; + error?: typeof console.error; +} = {}; + +// 获取调用堆栈信息的工厂函数 +const createGetCallerInfo = (trace: boolean) => (): NodeJS.CallSite | null => { + if (!trace) { + return null; + } + const originalFunc = Error.prepareStackTrace; + let callerInfo: NodeJS.CallSite | null = null; + try { + const err = new Error(); + Error.prepareStackTrace = (err, stack) => stack; + const stack = err.stack as unknown as NodeJS.CallSite[]; + const currentFile = stack[0].getFileName(); + + for (let i = 1; i < stack.length; i++) { + const callSite = stack[i]; + if (currentFile !== callSite.getFileName()) { + callerInfo = callSite; + break; + } + } + } catch (e) { + console.error(e); + } + Error.prepareStackTrace = originalFunc; + return callerInfo; +}; + +export const polyfillConsole = (id: string, trace: boolean, formatter?: LogFormatter) => { + // 检查是否已经添加过相同 id 的 polyfill + if (polyfillStack.some(item => item.id === id)) { + return; + } + + // 第一次调用时保存原始方法 + if (polyfillStack.length === 0) { + originalConsoleMethods.info = console.info; + originalConsoleMethods.warn = console.warn; + originalConsoleMethods.error = console.error; + } + + // 添加新的 polyfill 到栈顶 + polyfillStack.push({ id, trace, formatter }); + + // 重新设置 console 方法 + ["info", "warn", "error"].forEach((level: string) => { + const levelStr = level as "info" | "warn" | "error"; + const originalFunc = originalConsoleMethods[levelStr]!; + + console[levelStr] = function (...args) { + let processedArgs = args; + + // 从后往前执行所有 formatter + for (let i = polyfillStack.length - 1; i >= 0; i--) { + const item = polyfillStack[i]; + if (item.formatter) { + const getCallerInfo = createGetCallerInfo(item.trace); + processedArgs = item.formatter({ + level: levelStr, + caller: getCallerInfo(), + args: processedArgs, + }); + } + } + + originalFunc(...processedArgs); + }; + }); + + console.info(`new console polyfill added: ${id}`); +}; + +// 可选:提供移除 polyfill 的功能 +export const removePolyfill = (id: string) => { + const index = polyfillStack.findIndex(item => item.id === id); + if (index === -1) { + return; + } + + polyfillStack.splice(index, 1); + + // 如果栈为空,恢复原始方法 + if (polyfillStack.length === 0) { + console.info = originalConsoleMethods.info!; + console.warn = originalConsoleMethods.warn!; + console.error = originalConsoleMethods.error!; + } else { + // 否则重新应用所有 polyfill + const tempStack = [...polyfillStack]; + polyfillStack.length = 0; + Object.keys(originalConsoleMethods).forEach(level => { + const levelStr = level as "info" | "warn" | "error"; + console[levelStr] = originalConsoleMethods[levelStr]!; + }); + tempStack.forEach(item => { + polyfillStack.length = 0; // 清空以便重新添加 + polyfillConsole(item.id, item.trace, item.formatter); + }); + } +}; \ No newline at end of file diff --git a/src/server/start.ts b/src/server/start.ts index fb8f858..6911d9d 100644 --- a/src/server/start.ts +++ b/src/server/start.ts @@ -25,6 +25,8 @@ import mount from 'koa-mount'; import chalk from 'chalk'; import { checkNodeVersion, randomString } from '../utils'; import bcrypt from 'bcryptjs'; +import { LogFormatter, polyfillConsole, removePolyfill } from './polyfill'; +import { ErrorHandler } from '../types'; checkNodeVersion() @@ -54,6 +56,14 @@ export async function startup) => Promise, ): Promise<(() => Promise) | any> { + + const errorHandler: ErrorHandler = require(join( + path, + 'lib', + 'configuration', + 'errors' + )).default; + const serverConfiguration: ServerConfiguration = require(join( path, 'lib', @@ -204,6 +214,17 @@ export async function startup { + if (props.level === "error") { + appLoader.execRoutine(async (ctx) => { + await errorHandler( props.caller, props.args, ctx); + }); + } + return props.args; + }); + } + // 否则启动服务器模式 koa.use(async (ctx, next) => { try { @@ -451,6 +472,8 @@ export async function startup any[]; export type WatchConfig = { autoUpdateI18n?: boolean; @@ -856,53 +850,9 @@ export const watch = ( }; }; - const polyfillConsole = (trace: boolean) => { - // 获取调用堆栈信息 - const getCallerInfo = (): NodeJS.CallSite | null => { - if (!trace) { - return null; - } - const originalFunc = Error.prepareStackTrace; - let callerInfo: NodeJS.CallSite | null = null; - try { - const err = new Error(); - Error.prepareStackTrace = (err, stack) => stack; - const stack = err.stack as unknown as NodeJS.CallSite[]; // Type assertion here - const currentFile = stack[0].getFileName(); - - for (let i = 1; i < stack.length; i++) { - // Start from index 1 - const callSite = stack[i]; - if (currentFile !== callSite.getFileName()) { - callerInfo = callSite; - break; - } - } - } catch (e) { - console.error(e); - } - Error.prepareStackTrace = originalFunc; - return callerInfo; - }; - // polyfill console.log 添加时间和文件位置 - ["info", "warn", "error"].forEach((level: string) => { - const levelStr = level as "info" | "warn" | "error"; - const oldFunc = console[levelStr]; - console[levelStr] = function (...args) { - oldFunc( - ...(realConfig.polyfill?.console?.formatter({ - level: levelStr, - caller: getCallerInfo(), - args, - }) || []) - ); - }; - }); - }; - // 初始化polyfill polyfillLoader(); - realConfig.polyfill.console.enable && polyfillConsole(enableTrace); + realConfig.polyfill.console.enable && polyfillConsole("watch", enableTrace, realConfig.polyfill?.console?.formatter); // 初始编译检查 const initialCompile = () => { diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..0b41650 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,5 @@ +import { EntityDict } from "oak-domain/lib/types"; +import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; +import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; + +export type ErrorHandler = ( caller: NodeJS.CallSite | null, args: any[], ctx: AsyncContext) => Promise; \ No newline at end of file