Compare commits

..

31 Commits
4.0.28 ... dev

Author SHA1 Message Date
Pan Qiancheng 3efd2fe9dd fix: 修复部分历史遗留问题 2026-01-23 16:59:16 +08:00
Xu Chang eb39114fe9 4.0.33-dev 2026-01-23 08:49:07 +08:00
Xu Chang 152d20efe4 4.0.32-pub 2026-01-23 08:47:58 +08:00
Pan Qiancheng 96d4337b59 fix: 在处理异常的中间件中未正确处理serializeException返回的headers 2026-01-22 10:42:11 +08:00
Pan Qiancheng fc5f551cab Merge branch 'dev' of https://gitea.51mars.com/Oak-Team/oak-cli into dev 2026-01-15 14:50:55 +08:00
Pan Qiancheng 89fe961434 fix: 修复在watch模式下会重复注册定时器的问题 2026-01-15 14:50:52 +08:00
Xu Chang 3c32d41347 4.0.32-dev 2026-01-09 16:19:34 +08:00
Xu Chang f3e6ed4917 4.0.31-pub 2026-01-09 16:17:22 +08:00
Pan Qiancheng 13384ab772 feat: 模板使用新build.js编译 2026-01-04 16:59:25 +08:00
Pan Qiancheng fb727c6b3c feat: 使用oak-domain的isOakException函数,替代instanceof去判断err的类型 2025-12-22 14:23:53 +08:00
Pan Qiancheng 35095a0219 fix: 修复了非ts文件修改时,未处理源文件路径导致误删除的bug 2025-12-20 14:42:19 +08:00
Pan Qiancheng 09d14be5e7 feat: 适配新connector定义,支持ExposeHeaders,connector的自定义Aspect(用于实现前后台的connector初始化逻辑) 2025-12-17 15:04:57 +08:00
Xu Chang 22289f04d4 4.0.31-dev 2025-12-10 18:11:04 +08:00
Xu Chang 52b3ed8d97 4.0.30-pub 2025-12-10 18:09:48 +08:00
QCQCQC@Debian 3e0dba7172 feat: 修复stopRoutine重复执行,并添加模板文件 2025-12-10 10:55:41 +08:00
QCQCQC@Debian 3781ed4629 feat: 新增stopRoutines的执行,在shutdown时执行,并完善信号处理 2025-12-10 10:13:58 +08:00
Xu Chang 0e023f9a88 4.0.30-dev 2025-12-02 10:54:22 +08:00
Xu Chang 2b0ffbffaf 4.0.29-pub 2025-12-02 10:53:20 +08:00
Xu Chang da0bca5394 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-cli into dev 2025-11-28 10:43:13 +08:00
Xu Chang 4140595e0e template中一个疏漏 2025-11-28 10:43:10 +08:00
Pan Qiancheng ef0d846f0a feat: 完善watch编译器,现编译完成之后会自动进行alias替换,防止运行时问题,并修复server启动失败导致整体进程退出的问题 2025-11-26 17:09:10 +08:00
Pan Qiancheng 1440147f3e fix: 将自定义中间件移动到koa-body之后 2025-11-11 18:13:07 +08:00
Pan Qiancheng 896d53561e feat: serverConfiguration支持middleware配置 2025-11-11 18:02:54 +08:00
Pan Qiancheng f0671efef6 feat: 改为从appLoader加载ExceptionHandler 2025-11-11 11:03:39 +08:00
Pan Qiancheng 2d8690abb6 feat: 异常处理器改为从lib/configuration/exception导入 2025-11-07 10:01:49 +08:00
Pan Qiancheng 23865e639d feat: 现将异常处理器注册到appLoader,不再重载console.error 2025-11-06 15:34:20 +08:00
Pan Qiancheng 1939358367 fix: 复制项目时要带有.gitignore和oak.config.json两个文件 2025-11-04 13:45:29 +08:00
Pan Qiancheng 00dab2c454 fix: 修复模块化项目创建时的模板文件问题以及重命名项目名称问题
fix: 新增项目目录中不存在entities的检查
2025-11-04 10:28:15 +08:00
Pan Qiancheng e46c41812d fix: 复制模板文件时未同步等待 2025-11-04 10:10:20 +08:00
Pan Qiancheng 9a50ce03d3 fix: 修复在创建模块时更新configuration/compiler.js会出现报错的问题 2025-11-04 10:01:56 +08:00
Xu Chang 49b46c44a9 4.0.29-dev 2025-10-16 15:11:48 +08:00
20 changed files with 1130 additions and 256 deletions

View File

@ -191,8 +191,22 @@ async function create(dirName, cmd) {
(0, file_handle_1.checkFileExistsAndCreate)(rootPath); (0, file_handle_1.checkFileExistsAndCreate)(rootPath);
// 复制项目文件 // 复制项目文件
if (isModule) { if (isModule) {
// 模块化的项目只拷贝src和typings目录 // 模块化的项目,只拷贝 src 下的内容,但跳过 pages 目录;同时拷贝 typings
(0, file_handle_1.copyFolder)((0, path_1.join)(emptyTemplatePath, 'src'), (0, path_1.join)(rootPath, 'src')); const templateSrc = (0, path_1.join)(emptyTemplatePath, 'src');
const destSrc = (0, path_1.join)(rootPath, 'src');
// 确保目标 src 目录存在
if (!(0, fs_1.existsSync)(destSrc)) {
(0, fs_1.mkdirSync)(destSrc, { recursive: true });
}
const entries = (0, fs_1.readdirSync)(templateSrc, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === 'pages') {
continue; // 模块模式下跳过 pages
}
const from = (0, path_1.join)(templateSrc, entry.name);
const to = (0, path_1.join)(destSrc, entry.name);
(0, file_handle_1.copyFolder)(from, to);
}
(0, file_handle_1.copyFolder)((0, path_1.join)(emptyTemplatePath, 'typings'), (0, path_1.join)(rootPath, 'typings')); (0, file_handle_1.copyFolder)((0, path_1.join)(emptyTemplatePath, 'typings'), (0, path_1.join)(rootPath, 'typings'));
} }
else { else {
@ -234,8 +248,16 @@ async function create(dirName, cmd) {
(0, file_handle_1.checkFileExistsAndCreate)(tsConfigMpJsonPath, tsConfigMpJson, enum_1.checkFileExistsAndCreateType.FILE); (0, file_handle_1.checkFileExistsAndCreate)(tsConfigMpJsonPath, tsConfigMpJson, enum_1.checkFileExistsAndCreateType.FILE);
// 创建tsconfig.web.json // 创建tsconfig.web.json
(0, file_handle_1.checkFileExistsAndCreate)(tsConfigWebJsonPath, tsConfigWebJson, enum_1.checkFileExistsAndCreateType.FILE); (0, file_handle_1.checkFileExistsAndCreate)(tsConfigWebJsonPath, tsConfigWebJson, enum_1.checkFileExistsAndCreateType.FILE);
// 更新configuration/compiler.js // 复制.gitignore
(0, template_1.updateCompilerJsContent)(rootPath, deps); const gitignoreContent = (0, file_handle_1.readFile)((0, path_1.join)(__dirname, '..', 'template', '.gitignore'));
(0, file_handle_1.checkFileExistsAndCreate)((0, path_1.join)(rootPath, '.gitignore'), gitignoreContent, enum_1.checkFileExistsAndCreateType.FILE);
// 复制oak.config.json
const oakConfigContent = (0, file_handle_1.readFile)((0, path_1.join)(__dirname, '..', 'template', config_1.USER_CONFIG_FILE_NAME));
(0, file_handle_1.checkFileExistsAndCreate)((0, path_1.join)(rootPath, config_1.USER_CONFIG_FILE_NAME), oakConfigContent, enum_1.checkFileExistsAndCreateType.FILE);
// 更新configuration/compiler.js (仅在非模块化模式下)
if (!isModule) {
(0, template_1.updateCompilerJsContent)(rootPath, deps);
}
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Successfully created project ${(0, tip_style_1.primary)(name)}, directory name is ${(0, tip_style_1.primary)(dirName)}`)}`); (0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Successfully created project ${(0, tip_style_1.primary)(name)}, directory name is ${(0, tip_style_1.primary)(dirName)}`)}`);
shelljs_1.default.cd(dirName); shelljs_1.default.cd(dirName);
if (deps.length > 0) { if (deps.length > 0) {
@ -254,8 +276,21 @@ async function create(dirName, cmd) {
}); });
// 创建package.json // 创建package.json
(0, file_handle_1.checkFileExistsAndCreate)(packageJsonPath, packageJson, enum_1.checkFileExistsAndCreateType.FILE); (0, file_handle_1.checkFileExistsAndCreate)(packageJsonPath, packageJson, enum_1.checkFileExistsAndCreateType.FILE);
(0, rename_1.renameProject)(rootPath, name, title, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_TITLE); // 只在非模块化模式下重命名整个项目包括web、wechatMp等
if (example) { if (!isModule) {
(0, rename_1.renameProject)(rootPath, name, title, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_TITLE);
}
else {
// 模块化模式下只更新 package.json 的 name
const packageJsonFilePath = (0, path_1.join)(rootPath, 'package.json');
const packageJsonContent = (0, fs_1.readFileSync)(packageJsonFilePath, 'utf-8');
const packageJsonJson = JSON.parse(packageJsonContent);
packageJsonJson.name = name;
const newPackageJsonContent = JSON.stringify(packageJsonJson, undefined, 4);
(0, fs_1.writeFileSync)(packageJsonFilePath, newPackageJsonContent);
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Change project name to ${(0, tip_style_1.primary)(name)}`)}`);
}
if (example && !isModule) {
// todo: copy template example files // todo: copy template example files
(0, file_handle_1.copyFolder)(examplePath, rootPath, true); (0, file_handle_1.copyFolder)(examplePath, rootPath, true);
} }

View File

@ -37,7 +37,7 @@ export declare function writeFile(path: string | PathLike, data: any): void;
export declare function readFile(path: string | PathLike, options?: { export declare function readFile(path: string | PathLike, options?: {
encoding?: null | undefined; encoding?: null | undefined;
flag?: string | undefined; flag?: string | undefined;
} | null): Buffer<ArrayBufferLike> | undefined; } | null): NonSharedBuffer | undefined;
/** /**
* @name * @name
* @export * @export

View File

@ -149,9 +149,8 @@ function copyFolder(currentDir, targetDir, overwrite = false) {
} }
else { else {
if (file.isFile()) { if (file.isFile()) {
const readStream = (0, fs_1.createReadStream)(copyCurrentFileInfo); // 使用同步复制确保文件完全写入
const writeStream = (0, fs_1.createWriteStream)(copyTargetFileInfo); (0, fs_1.copyFileSync)(copyCurrentFileInfo, copyTargetFileInfo);
readStream.pipe(writeStream);
// console.log(`复制文件: ${copyCurrentFileInfo} -> ${copyTargetFileInfo}`); // console.log(`复制文件: ${copyCurrentFileInfo} -> ${copyTargetFileInfo}`);
} }
else { else {

View File

@ -9,7 +9,7 @@ const path_1 = require("path");
const koa_1 = tslib_1.__importDefault(require("koa")); const koa_1 = tslib_1.__importDefault(require("koa"));
const koa_router_1 = tslib_1.__importDefault(require("koa-router")); const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
const koa_body_1 = tslib_1.__importDefault(require("koa-body")); const koa_body_1 = tslib_1.__importDefault(require("koa-body"));
const koa_logger_1 = tslib_1.__importDefault(require("koa-logger")); // import logger from 'koa-logger';
const oak_backend_base_1 = require("oak-backend-base"); const oak_backend_base_1 = require("oak-backend-base");
const types_1 = require("oak-domain/lib/types"); const types_1 = require("oak-domain/lib/types");
const cluster_adapter_1 = require("@socket.io/cluster-adapter"); const cluster_adapter_1 = require("@socket.io/cluster-adapter");
@ -23,7 +23,6 @@ const koa_mount_1 = tslib_1.__importDefault(require("koa-mount"));
const chalk_1 = tslib_1.__importDefault(require("chalk")); const chalk_1 = tslib_1.__importDefault(require("chalk"));
const utils_1 = require("../utils"); const utils_1 = require("../utils");
const bcryptjs_1 = tslib_1.__importDefault(require("bcryptjs")); const bcryptjs_1 = tslib_1.__importDefault(require("bcryptjs"));
const polyfill_1 = require("./polyfill");
(0, utils_1.checkNodeVersion)(); (0, utils_1.checkNodeVersion)();
const socketAdminUI = (0, path_1.join)(__dirname, '../../ui/socket-admin'); const socketAdminUI = (0, path_1.join)(__dirname, '../../ui/socket-admin');
const DATA_SUBSCRIBE_NAMESPACE = '/dsn'; const DATA_SUBSCRIBE_NAMESPACE = '/dsn';
@ -39,13 +38,17 @@ function concat(...paths) {
}); });
} }
async function startup(path, connector, omitWatchers, omitTimers, routine) { async function startup(path, connector, omitWatchers, omitTimers, routine) {
let errorHandler = undefined; // let errorHandler: InternalErrorHandler<ED, Cxt> | undefined = undefined;
try { // try {
errorHandler = require((0, path_1.join)(path, 'lib', 'configuration', 'errors')).default; // errorHandler = require(join(
} // path,
catch (err) { // 'lib',
// 不存在errors配置 // 'configuration',
} // 'exception'
// )).default;
// } catch (err) {
// // 不存在exception配置
// }
const serverConfiguration = require((0, path_1.join)(path, 'lib', 'configuration', 'server')).default; const serverConfiguration = require((0, path_1.join)(path, 'lib', 'configuration', 'server')).default;
// 拿到package.json用作项目的唯一标识否则无法区分不同项目的Redis+socketIO连接 // 拿到package.json用作项目的唯一标识否则无法区分不同项目的Redis+socketIO连接
const packageJson = require((0, path_1.join)(path, 'package.json')); const packageJson = require((0, path_1.join)(path, 'package.json'));
@ -59,7 +62,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
const corsMethods = ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS']; const corsMethods = ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'];
const koa = new koa_1.default(); const koa = new koa_1.default();
// 使用 koa-logger 中间件打印日志 // 使用 koa-logger 中间件打印日志
koa.use((0, koa_logger_1.default)()); // koa.use(logger());
// socket // socket
const httpServer = (0, http_1.createServer)(koa.callback()); const httpServer = (0, http_1.createServer)(koa.callback());
const socketPath = connector.getSocketPath(); const socketPath = connector.getSocketPath();
@ -169,21 +172,25 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
if (routine) { if (routine) {
// 如果传入了routine执行完成后就结束 // 如果传入了routine执行完成后就结束
const result = await appLoader.execRoutine(routine); const result = await appLoader.execRoutine(routine);
await appLoader.unmount(); // await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
return result; return result;
} }
if (errorHandler && typeof errorHandler === 'function') { // if (errorHandler && typeof errorHandler === 'function') {
(0, polyfill_1.polyfillConsole)("startup", true, (props) => { // // polyfillConsole("startup", true, (props) => {
if (props.level === "error") { // // if (props.level === "error") {
appLoader.execRoutine(async (ctx) => { // // appLoader.execRoutine(async (ctx) => {
await errorHandler(props.caller, props.args, ctx); // // await errorHandler(props.caller, props.args, ctx);
}).catch((err) => { // // }).catch((err) => {
console.warn('执行全局错误处理失败:', err); // // console.warn('执行全局错误处理失败:', err);
}); // // });
} // // }
return props.args; // // return props.args;
}); // // });
} // // appLoader.registerInternalErrorHandler(async (ctx, type, msg, err) => {
// // await errorHandler(ctx, type, msg, err);
// // });
// }
appLoader.regAllExceptionHandler();
// 否则启动服务器模式 // 否则启动服务器模式
koa.use(async (ctx, next) => { koa.use(async (ctx, next) => {
try { try {
@ -192,18 +199,39 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
catch (err) { catch (err) {
console.error(err); console.error(err);
const { request } = ctx; const { request } = ctx;
const exception = err instanceof types_1.OakException const exception = (0, types_1.isOakException)(err)
? err ? err
: new types_1.OakException(serverConfiguration?.internalExceptionMask || : new types_1.OakException(serverConfiguration?.internalExceptionMask ||
ExceptionMask); ExceptionMask);
const { body } = connector.serializeException(exception, request.headers, request.body); const { body, headers } = connector.serializeException(exception, request.headers, request.body);
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key]);
});
return; return;
} }
}); });
koa.use((0, koa_body_1.default)(Object.assign({ koa.use((0, koa_body_1.default)(Object.assign({
multipart: true, multipart: true,
}, serverConfiguration.koaBody))); }, serverConfiguration.koaBody)));
// 注册自定义中间件
if (serverConfiguration.middleware) {
if (Array.isArray(serverConfiguration.middleware)) {
serverConfiguration.middleware.forEach((mw) => {
koa.use(mw);
});
}
else if (typeof serverConfiguration.middleware === 'function') {
const mws = serverConfiguration.middleware(koa);
if (!Array.isArray(mws)) {
throw new Error('middleware 配置函数必须返回 Koa.Middleware 数组');
}
mws.forEach((mw) => {
koa.use(mw);
});
}
}
const router = new koa_router_1.default(); const router = new koa_router_1.default();
// 如果是开发环境允许options // 如果是开发环境允许options
if (['development', 'staging'].includes(process.env.NODE_ENV)) { if (['development', 'staging'].includes(process.env.NODE_ENV)) {
@ -211,6 +239,10 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader())); ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
ctx.set('Access-Control-Allow-Methods', corsMethods); ctx.set('Access-Control-Allow-Methods', corsMethods);
if (connector.getCorsExposeHeaders) {
const exposeHeaders = connector.getCorsExposeHeaders();
ctx.set('Access-Control-Expose-Headers', exposeHeaders);
}
if (ctx.method == 'OPTIONS') { if (ctx.method == 'OPTIONS') {
ctx.body = 200; ctx.body = 200;
} }
@ -244,12 +276,39 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
} }
}); });
} }
const connectorCustomAspects = connector.registerCustomAspect ? connector.registerCustomAspect() : null;
router.post(connector.getRouter(), async (ctx) => { router.post(connector.getRouter(), async (ctx) => {
const { request } = ctx; const { request } = ctx;
const { contextString, aspectName, data } = connector.parseRequest(request.headers, request.body, request.files); const { contextString, aspectName, data } = await connector.parseRequest(request.headers, request.body, request.files);
const { result, opRecords, message } = await appLoader.execAspect(aspectName, request.headers, contextString, data); let result;
let opRecords = [];
let message = undefined;
if (connectorCustomAspects &&
connectorCustomAspects.findIndex(a => a.name === aspectName) >= 0) {
// 自定义aspect处理
console.log(`调用Connector自定义Aspect: ${aspectName}`);
const aspect = connectorCustomAspects.find(a => a.name === aspectName);
const res = await aspect.handler({
headers: request.headers,
contextString,
params: data,
});
result = res.result;
opRecords = res.opRecords || [];
message = res.message;
}
else {
const res = await appLoader.execAspect(aspectName, request.headers, contextString, data);
result = res.result;
opRecords = res.opRecords || [];
message = res.message;
}
const { body, headers } = await connector.serializeResult(result, opRecords, request.headers, request.body, message); const { body, headers } = await connector.serializeResult(result, opRecords, request.headers, request.body, message);
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key]);
});
return; return;
}); });
// 桥接访问外部资源的入口 // 桥接访问外部资源的入口
@ -377,15 +436,63 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
if (!omitTimers) { if (!omitTimers) {
appLoader.startTimers(); appLoader.startTimers();
} }
process.on('SIGINT', async () => { let isShutingdown = false;
await appLoader.unmount();
process.exit(0);
});
const shutdown = async () => { const shutdown = async () => {
await httpServer.close(); if (isShutingdown) {
await koa.removeAllListeners(); return;
await appLoader.unmount(); }
(0, polyfill_1.removePolyfill)("startup"); isShutingdown = true;
console.log('服务器正在关闭中...');
try {
await httpServer.close();
await koa.removeAllListeners();
await appLoader.execStopRoutines();
await appLoader.unmount();
}
catch (err) {
console.error('关闭服务器时出错:', err);
}
}; };
// 处理优雅关闭的统一入口
let shutdownStarted = false;
const handleShutdown = async (signal) => {
// 防止重复处理
if (shutdownStarted) {
console.warn('关闭流程已启动,忽略此信号');
return;
}
shutdownStarted = true;
console.log(`\n收到 ${signal} 信号,准备关闭服务器...`);
// 移除所有信号处理器,防止重复触发
process.removeAllListeners('SIGINT');
process.removeAllListeners('SIGTERM');
process.removeAllListeners('SIGQUIT');
process.removeAllListeners('SIGHUP');
try {
await shutdown();
process.exit(0);
}
catch (err) {
console.error('关闭过程出错:', err);
process.exit(1);
}
};
// 监听终止信号进行优雅关闭
// SIGINT - Ctrl+C 发送的中断信号
process.on('SIGINT', () => {
handleShutdown('SIGINT');
});
// SIGTERM - 系统/容器管理器发送的终止信号Docker、K8s、PM2等
process.on('SIGTERM', () => {
handleShutdown('SIGTERM');
});
// SIGQUIT - Ctrl+\ 发送的退出信号
process.on('SIGQUIT', () => {
handleShutdown('SIGQUIT');
});
// SIGHUP - 终端关闭时发送(可选)
process.on('SIGHUP', () => {
handleShutdown('SIGHUP');
});
return shutdown; return shutdown;
} }

45
lib/server/timer-manager.d.ts vendored Normal file
View File

@ -0,0 +1,45 @@
type TimerType = 'timeout' | 'interval' | 'immediate';
interface TimerRecord {
id: NodeJS.Timeout | NodeJS.Immediate;
type: TimerType;
createdAt: number;
}
declare class GlobalTimerManager {
private timers;
private isHooked;
private readonly original;
/**
*
*/
hook(): void;
/**
*
*/
unhook(): void;
/**
*
*/
clearAll(): number;
/**
*
*/
clearByType(type: TimerType): number;
/**
*
*/
getActiveCount(): number;
/**
*
*/
getStats(): Record<TimerType, number>;
/**
*
*/
getTimers(): TimerRecord[];
}
export declare const timerManager: GlobalTimerManager;
export declare const hookTimers: () => void;
export declare const unhookTimers: () => void;
export declare const clearAllTimers: () => number;
export declare const getTimerStats: () => Record<TimerType, number>;
export {};

162
lib/server/timer-manager.js Normal file
View File

@ -0,0 +1,162 @@
"use strict";
// timer-manager.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTimerStats = exports.clearAllTimers = exports.unhookTimers = exports.hookTimers = exports.timerManager = void 0;
class GlobalTimerManager {
timers = new Map();
isHooked = false;
// 保存原始函数
original = {
setTimeout: global.setTimeout,
setInterval: global.setInterval,
setImmediate: global.setImmediate, clearTimeout: global.clearTimeout,
clearInterval: global.clearInterval,
clearImmediate: global.clearImmediate,
};
/**
* 开始拦截全局定时器
*/
hook() {
if (this.isHooked) {
console.warn('[TimerManager] Already hooked');
return;
}
const self = this;
// Hook setTimeout
global.setTimeout = function (callback, ms, ...args) {
const id = self.original.setTimeout((...callbackArgs) => {
self.timers.delete(id); // 执行完后移除
callback(...callbackArgs);
}, ms, ...args);
self.timers.set(id, {
id,
type: 'timeout',
createdAt: Date.now(),
});
return id;
};
// Hook setInterval
global.setInterval = function (callback, ms, ...args) {
const id = self.original.setInterval(callback, ms, ...args);
self.timers.set(id, {
id,
type: 'interval',
createdAt: Date.now(),
});
return id;
};
// Hook setImmediate
global.setImmediate = function (callback, ...args) {
const id = self.original.setImmediate((...callbackArgs) => {
self.timers.delete(id); // 执行完后移除
callback(...callbackArgs);
}, ...args);
self.timers.set(id, {
id,
type: 'immediate',
createdAt: Date.now(),
});
return id;
};
// Hook clear方法确保从追踪中移除
global.clearTimeout = ((id) => {
self.timers.delete(id);
return self.original.clearTimeout(id);
});
global.clearInterval = ((id) => {
self.timers.delete(id);
return self.original.clearInterval(id);
});
global.clearImmediate = ((id) => {
self.timers.delete(id);
return self.original.clearImmediate(id);
});
this.isHooked = true;
console.log('[TimerManager] Hooked global timer functions');
}
/**
* 恢复原始的全局定时器函数
*/
unhook() {
if (!this.isHooked) {
return;
}
global.setTimeout = this.original.setTimeout;
global.setInterval = this.original.setInterval;
global.setImmediate = this.original.setImmediate;
global.clearTimeout = this.original.clearTimeout;
global.clearInterval = this.original.clearInterval;
global.clearImmediate = this.original.clearImmediate;
this.isHooked = false;
}
/**
* 清除所有被追踪的定时器
*/
clearAll() {
const count = this.timers.size;
this.timers.forEach((record) => {
if (record.type === 'immediate') {
this.original.clearImmediate.call(null, record.id);
}
else {
this.original.clearTimeout.call(null, record.id);
}
});
this.timers.clear();
return count;
}
/**
* 按类型清除定时器
*/
clearByType(type) {
let count = 0;
this.timers.forEach((record, id) => {
if (record.type === type) {
if (type === 'immediate') {
this.original.clearImmediate.call(null, id);
}
else {
this.original.clearTimeout.call(null, id);
}
this.timers.delete(id);
count++;
}
});
return count;
}
/**
* 获取当前活跃的定时器数量
*/
getActiveCount() {
return this.timers.size;
}
/**
* 获取定时器统计信息
*/
getStats() {
const stats = {
timeout: 0,
interval: 0,
immediate: 0,
};
this.timers.forEach((record) => {
stats[record.type]++;
});
return stats;
}
/**
* 获取所有定时器的详细信息用于调试
*/
getTimers() {
return Array.from(this.timers.values());
}
}
exports.timerManager = new GlobalTimerManager();
const hookTimers = () => exports.timerManager.hook();
exports.hookTimers = hookTimers;
const unhookTimers = () => exports.timerManager.unhook();
exports.unhookTimers = unhookTimers;
const clearAllTimers = () => exports.timerManager.clearAll();
exports.clearAllTimers = clearAllTimers;
const getTimerStats = () => exports.timerManager.getStats();
exports.getTimerStats = getTimerStats;

View File

@ -7,8 +7,9 @@ const typescript_1 = tslib_1.__importDefault(require("typescript"));
const path_1 = tslib_1.__importDefault(require("path")); const path_1 = tslib_1.__importDefault(require("path"));
const dayjs_1 = tslib_1.__importDefault(require("dayjs")); const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
const fs_1 = tslib_1.__importDefault(require("fs")); const fs_1 = tslib_1.__importDefault(require("fs"));
const lodash_1 = require("lodash");
const polyfill_1 = require("./polyfill"); const polyfill_1 = require("./polyfill");
const tsc_alias_1 = require("tsc-alias");
const timer_manager_1 = require("./timer-manager");
// 创建事件发射器 // 创建事件发射器
const createEventEmitter = () => { const createEventEmitter = () => {
const listeners = new Map(); const listeners = new Map();
@ -227,12 +228,20 @@ const createServerManager = (projectPath, eventEmitter, config) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default; const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
console.warn("----> Starting service......"); console.warn("----> Starting service......");
// 这里注意要在require之前因为require会触发编译 try {
const { startup } = require('./start'); (0, timer_manager_1.clearAllTimers)();
shutdown = await startup(pwd, simpleConnector).then((shutdown) => { // 这里注意要在require之前因为require会触发编译
config.lifecycle.onServerStart(config); const { startup } = require('./start');
return shutdown; shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
}); config.lifecycle.onServerStart(config);
return shutdown;
});
}
catch (error) {
console.error("----> Failed to start service:", error);
isRestarting = false;
return;
}
isRestarting = false; isRestarting = false;
await eventEmitter.emit("server-restarted", {}); await eventEmitter.emit("server-restarted", {});
}; };
@ -248,7 +257,7 @@ const createServerManager = (projectPath, eventEmitter, config) => {
}; };
}; };
// 创建编译器 // 创建编译器
const createCompiler = (projectPath, options, projectReferences, aliasConfig, config) => { const createCompiler = async (projectPath, options, projectReferences, treatFile, config) => {
const createProgramAndSourceFile = (path) => { const createProgramAndSourceFile = (path) => {
const program = typescript_1.default.createProgram({ const program = typescript_1.default.createProgram({
rootNames: [path], rootNames: [path],
@ -273,11 +282,12 @@ const createCompiler = (projectPath, options, projectReferences, aliasConfig, co
}; };
const compileTask = async (task) => { const compileTask = async (task) => {
const { filePath, changeType } = task; const { filePath, changeType } = task;
const modulePath = path_1.default.resolve(filePath);
// 判断文件类型 // 判断文件类型
if (!filePath.endsWith(".ts")) { if (!filePath.endsWith(".ts")) {
// 处理非TypeScript文件 (如JSON文件) // 处理非TypeScript文件 (如JSON文件)
if (filePath.endsWith(".json")) { if (filePath.endsWith(".json")) {
const targetPath = filePath.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")); const targetPath = modulePath.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"));
try { try {
if (changeType === "remove") { if (changeType === "remove") {
if (fs_1.default.existsSync(targetPath)) { if (fs_1.default.existsSync(targetPath)) {
@ -324,7 +334,6 @@ const createCompiler = (projectPath, options, projectReferences, aliasConfig, co
// 处理TypeScript文件 // 处理TypeScript文件
console.clear(); console.clear();
console.warn(`File ${filePath} has been ${changeType}d`); console.warn(`File ${filePath} has been ${changeType}d`);
const modulePath = path_1.default.resolve(filePath);
const libPath = modulePath const libPath = modulePath
.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")) .replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"))
.replace(/\.ts$/, ".js"); .replace(/\.ts$/, ".js");
@ -375,6 +384,8 @@ const createCompiler = (projectPath, options, projectReferences, aliasConfig, co
else { else {
console.log(`Emit succeeded for ${filePath}.`); console.log(`Emit succeeded for ${filePath}.`);
config.lifecycle.onAfterCompile(config); config.lifecycle.onAfterCompile(config);
const jsFilePath = libPath;
treatFile(jsFilePath);
return { return {
taskId: task.id, taskId: task.id,
success: true, success: true,
@ -445,40 +456,39 @@ const createFileWatcher = (projectPath, eventEmitter) => {
const generateTaskId = () => { const generateTaskId = () => {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}; };
/** // /**
* 根据 alias 配置表将路径中的别名替换为真实路径 // * 根据 alias 配置表将路径中的别名替换为真实路径
* @param path - 输入的路径字符串例如 "@project/file" "@oak-app-domain/some-module" // * @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
* @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组 // * @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组
* @returns 替换后的路径如果没有匹配到 alias则返回原始路径 // * @returns 替换后的路径,如果没有匹配到 alias则返回原始路径
*/ // */
const replaceAliasWithPath = (path, aliasConfig) => { // const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => {
for (const [alias, targets] of Object.entries(aliasConfig)) { // for (const [alias, targets] of Object.entries(aliasConfig)) {
// If alias ends with "*", handle it as a dynamic alias. // // If alias ends with "*", handle it as a dynamic alias.
if (alias.endsWith('*')) { // if (alias.endsWith('*')) {
// Create a regex pattern that matches paths starting with the alias, followed by any characters // // Create a regex pattern that matches paths starting with the alias, followed by any characters
const aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)' // const aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)'
const match = path.match(aliasPattern); // const match = path.match(aliasPattern);
if (match) { // if (match) {
// Replace the alias with the target path, appending the matched part from the original path // // Replace the alias with the target path, appending the matched part from the original path
const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array) // const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
// Ensure that the target path ends with a slash if it's not already // // Ensure that the target path ends with a slash if it's not already
const replacedPath = target.replace(/\/\*$/, "/") + match[1]; // const replacedPath = target.replace(/\/\*$/, "/") + match[1];
return replacedPath; // return replacedPath;
} // }
} // } else {
else { // // Handle static alias without "*" by directly matching the path
// Handle static alias without "*" by directly matching the path // if (path.startsWith(alias)) {
if (path.startsWith(alias)) { // const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array) // // Replace the alias part with the target path
// Replace the alias part with the target path // return path.replace(alias, target);
return path.replace(alias, target); // }
} // }
} // }
} // // If no alias matches, return the original path
// If no alias matches, return the original path // return path;
return path; // };
}; const watch = async (projectPath, config) => {
const watch = (projectPath, config) => {
const realConfig = getOverrideConfig(config); const realConfig = getOverrideConfig(config);
const enableTrace = !!process.env.ENABLE_TRACE; const enableTrace = !!process.env.ENABLE_TRACE;
// 查找配置文件 // 查找配置文件
@ -486,20 +496,31 @@ const watch = (projectPath, config) => {
if (!configFileName) { if (!configFileName) {
throw new Error("Could not find a valid 'tsconfig.build.json'."); throw new Error("Could not find a valid 'tsconfig.build.json'.");
} }
// 读取配置文件 const runFile = await (0, tsc_alias_1.prepareSingleFileReplaceTscAliasPaths)({
const configFile = typescript_1.default.readConfigFile(configFileName, typescript_1.default.sys.readFile); configFile: path_1.default.join(projectPath, "tsconfig.build.json"),
// 解析配置文件内容 resolveFullPaths: true,
const { options, projectReferences } = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, path_1.default.dirname(configFileName));
const aliasConfig = (0, lodash_1.cloneDeep)(options.paths) || {};
// 输出原始配置
// console.log("[DEBUG] Original alias config:", aliasConfig);
Object.keys(aliasConfig).forEach((key) => {
const value = aliasConfig[key];
// 替换src
aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v) => v.replace("src", "lib"));
}); });
function treatFile(filePath) {
console.log(`Processing file for alias replacement: ${filePath}`);
const fileContents = fs_1.default.readFileSync(filePath, 'utf8');
const newContents = runFile({ fileContents, filePath });
// do stuff with newContents
fs_1.default.writeFileSync(filePath, newContents, 'utf8');
}
// // 读取配置文件
const configFile = typescript_1.default.readConfigFile(configFileName, typescript_1.default.sys.readFile);
// // 解析配置文件内容
const { options, projectReferences } = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, path_1.default.dirname(configFileName));
// const aliasConfig: AliasConfig = cloneDeep(options.paths) || {};
// // 输出原始配置
// // console.log("[DEBUG] Original alias config:", aliasConfig);
// Object.keys(aliasConfig).forEach((key) => {
// const value = aliasConfig[key];
// // 替换src
// aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v: string) => v.replace("src", "lib"));
// });
// 输出真实的alias配置 // 输出真实的alias配置
console.debug("[DEBUG] Running Alias config:", aliasConfig); // console.debug("[DEBUG] Running Alias config:", aliasConfig);
// 初始化polyfill // 初始化polyfill
const polyfillLoader = () => { const polyfillLoader = () => {
const BuiltinModule = require("module"); const BuiltinModule = require("module");
@ -530,6 +551,9 @@ const watch = (projectPath, config) => {
console.error(`[resolve] Emit skipped for: ${tsPath}`); console.error(`[resolve] Emit skipped for: ${tsPath}`);
throw new Error("TypeScript emit skipped"); throw new Error("TypeScript emit skipped");
} }
else {
treatFile(jsPath);
}
console.log(`[resolve] Successfully compiled: ${tsPath}`); console.log(`[resolve] Successfully compiled: ${tsPath}`);
return jsPath; return jsPath;
} }
@ -559,11 +583,11 @@ const watch = (projectPath, config) => {
rFoptions // 解析选项 rFoptions // 解析选项
) { ) {
let resolvedRequest = request; let resolvedRequest = request;
const replacedPath = replaceAliasWithPath(request, aliasConfig); // const replacedPath = replaceAliasWithPath(request, aliasConfig);
if (replacedPath !== request) { // if (replacedPath !== request) {
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`); // console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
resolvedRequest = path_1.default.join(projectPath, replacedPath); // resolvedRequest = pathLib.join(projectPath, replacedPath);
} // }
try { try {
return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions); return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
} }
@ -584,6 +608,8 @@ const watch = (projectPath, config) => {
// 初始化polyfill // 初始化polyfill
polyfillLoader(); polyfillLoader();
realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter); realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter);
// 监听定时器相关的API,确保定时器在重启后不会重复触发
(0, timer_manager_1.hookTimers)();
// 初始编译检查 // 初始编译检查
const initialCompile = () => { const initialCompile = () => {
const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js"); const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js");
@ -631,14 +657,34 @@ const watch = (projectPath, config) => {
"src/ports/index.ts", "src/ports/index.ts",
]; ];
compileFiles.forEach(tryCompile); compileFiles.forEach(tryCompile);
// 最后替换lib目录下所有的路径别名
console.log(`[watch] Replacing path aliases in lib directory......`);
const libDir = path_1.default.join(projectPath, "lib");
const walkDir = (dir) => {
const files = fs_1.default.readdirSync(dir);
files.forEach((file) => {
const fullPath = path_1.default.join(dir, file);
const stat = fs_1.default.statSync(fullPath);
if (stat.isDirectory()) {
walkDir(fullPath);
}
else if (stat.isFile() && fullPath.endsWith(".js")) {
const fileContents = fs_1.default.readFileSync(fullPath, 'utf8');
const newContents = runFile({ fileContents, filePath: fullPath });
fs_1.default.writeFileSync(fullPath, newContents, 'utf8');
}
});
};
walkDir(libDir);
console.log(`[watch] Path alias replacement completed.`);
} }
}; };
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
// 创建事件系统 // 创建事件系统
const eventEmitter = createEventEmitter(); const eventEmitter = createEventEmitter();
// 创建各个组件 // 创建各个组件
const compileQueue = createCompileQueue(eventEmitter); const compileQueue = createCompileQueue(eventEmitter);
const compiler = createCompiler(projectPath, options, projectReferences, aliasConfig, realConfig); const compiler = await createCompiler(projectPath, options, projectReferences, treatFile, realConfig);
const serverManager = createServerManager(projectPath, eventEmitter, realConfig); const serverManager = createServerManager(projectPath, eventEmitter, realConfig);
const fileWatcher = createFileWatcher(projectPath, eventEmitter); const fileWatcher = createFileWatcher(projectPath, eventEmitter);
// 设置编译器处理器 // 设置编译器处理器

View File

@ -85,7 +85,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze", "build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
"build:mp": "${cliBinName} build --target mp --mode production", "build:mp": "${cliBinName} build --target mp --mode production",
"build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze", "build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze",
"build:watch": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch", "build:watch": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch",
"start:web": "${cliBinName} start --target web --mode development --devMode frontend", "start:web": "${cliBinName} start --target web --mode development --devMode frontend",
"start:web:server": "${cliBinName} start --target web --mode development", "start:web:server": "${cliBinName} start --target web --mode development",
"start:native": "${cliBinName} start --target rn --mode development --devMode frontend", "start:native": "${cliBinName} start --target rn --mode development --devMode frontend",
@ -97,7 +97,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
"build-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap", "build-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap",
"build-analyze:web": "${cliBinName} build --target web --mode production --analyze", "build-analyze:web": "${cliBinName} build --target web --mode production --analyze",
"build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --analyze", "build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --analyze",
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json", "build": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json",
"prebuild": "npm run make:locale", "prebuild": "npm run make:locale",
"run:ios": "oak-cli run -p ios", "run:ios": "oak-cli run -p ios",
"run:android": "oak-cli run -p android", "run:android": "oak-cli run -p android",
@ -397,7 +397,13 @@ function tsConfigBuildJsonContent() {
"test", "test",
"src/pages/**/*", "src/pages/**/*",
"src/components/**/*" "src/components/**/*"
] ],
"oakBuildChecks": {
"context": {
"checkAsyncContext": true,
"targetModules": ["context/BackendRuntimeContext"]
}
}
}`; }`;
} }
function tsConfigPathsJsonContent(deps) { function tsConfigPathsJsonContent(deps) {
@ -424,7 +430,7 @@ function tsConfigPathsJsonContent(deps) {
compilerOptions: { compilerOptions: {
baseUrl: "./", baseUrl: "./",
paths, paths,
typeRoots: ["./typings"] typeRoots: ["./typings", "node_modules/@types"]
} }
}, null, '\t'); }, null, '\t');
} }
@ -636,8 +642,13 @@ function oakConfigContentWithWeb() {
} }
function updateCompilerJsContent(directory, deps) { function updateCompilerJsContent(directory, deps) {
const compilerJsPath = (0, path_1.join)(directory, 'configuration', 'compiler.js'); const compilerJsPath = (0, path_1.join)(directory, 'configuration', 'compiler.js');
(0, assert_1.default)((0, fs_1.existsSync)(compilerJsPath)); // 只有在有依赖项时才需要修改 compiler.js
if (deps.length > 0) { if (deps.length > 0) {
// 检查文件是否存在
if (!(0, fs_1.existsSync)(compilerJsPath)) {
console.warn(`Warning: ${compilerJsPath} does not exist, skipping compiler.js update`);
return;
}
const { ast } = (0, core_1.transformFileSync)(compilerJsPath, { ast: true }); const { ast } = (0, core_1.transformFileSync)(compilerJsPath, { ast: true });
const { program } = ast; const { program } = ast;
const { body } = program; const { body } = program;

View File

@ -1,4 +1,6 @@
import { BaseEntityDict } from "oak-domain";
import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore";
import { EntityDict } from "oak-domain/lib/types"; import { EntityDict } from "oak-domain/lib/types";
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; export type InternalErrorType = 'aspect' | 'trigger' | 'watcher' | 'timer' | 'checkpoint';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; export type InternalErrorHandler<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> = (ctx: Cxt, type: InternalErrorType, message: string, err: Error) => Promise<void>;
export type ErrorHandler<ED extends EntityDict & BaseEntityDict> = (caller: NodeJS.CallSite | null, args: any[], ctx: AsyncContext<ED>) => Promise<void>; export type ExceptionPublisher = (type: string, message: string, err: any) => Promise<void>;

View File

@ -1,6 +1,6 @@
{ {
"name": "@xuchangzju/oak-cli", "name": "@xuchangzju/oak-cli",
"version": "4.0.28", "version": "4.0.33",
"description": "client for oak framework", "description": "client for oak framework",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
@ -44,7 +44,7 @@
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "^5.0.0",
"events": "^3.3.0", "events": "^3.3.0",
"fork-ts-checker-webpack-plugin": "^8.0.0", "fork-ts-checker-webpack-plugin": "^8.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.2",
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, },
"dependencies": { "dependencies": {
@ -112,9 +112,9 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.5.3", "mini-css-extract-plugin": "^2.5.3",
"node-watch": "^0.7.4", "node-watch": "^0.7.4",
"oak-backend-base": "^4.1.23", "oak-backend-base": "file:../oak-backend-base",
"oak-domain": "^5.1.28", "oak-domain": "file:../oak-domain",
"oak-frontend-base": "^5.3.38", "oak-frontend-base": "file:../oak-frontend-base",
"parse-asn1": "5.1.6", "parse-asn1": "5.1.6",
"postcss": "^8.4.4", "postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
@ -145,6 +145,7 @@
"stylelint-webpack-plugin": "^3.2.0", "stylelint-webpack-plugin": "^3.2.0",
"tailwindcss": "^3.0.2", "tailwindcss": "^3.0.2",
"terser-webpack-plugin": "^5.2.5", "terser-webpack-plugin": "^5.2.5",
"tsc-alias": "^1.8.16",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"ui-extract-webpack-plugin": "^1.0.0", "ui-extract-webpack-plugin": "^1.0.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",

View File

@ -19,6 +19,12 @@ dependencies.forEach(
} }
); );
analyzeEntities(join(process.cwd(), 'src', 'entities')); const projectEntitiesPath = join(process.cwd(), 'src', 'entities');
if (existsSync(projectEntitiesPath)) {
analyzeEntities(projectEntitiesPath, 'src/entities');
} else {
console.warn('no project entities found');
}
removeSync(join(process.cwd(), 'src', 'oak-app-domain')); removeSync(join(process.cwd(), 'src', 'oak-app-domain'));
buildSchema(join(process.cwd(), 'src', 'oak-app-domain')); buildSchema(join(process.cwd(), 'src', 'oak-app-domain'));

View File

@ -1,5 +1,5 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import { writeFileSync } from 'fs'; import { writeFileSync, readFileSync, readdirSync, existsSync, mkdirSync } from 'fs';
const { factory } = ts; const { factory } = ts;
import { import {
@ -279,8 +279,22 @@ export async function create(dirName: string, cmd: any) {
checkFileExistsAndCreate(rootPath); checkFileExistsAndCreate(rootPath);
// 复制项目文件 // 复制项目文件
if (isModule) { if (isModule) {
// 模块化的项目只拷贝src和typings目录 // 模块化的项目,只拷贝 src 下的内容,但跳过 pages 目录;同时拷贝 typings
copyFolder(join(emptyTemplatePath, 'src'), join(rootPath, 'src')); const templateSrc = join(emptyTemplatePath, 'src');
const destSrc = join(rootPath, 'src');
// 确保目标 src 目录存在
if (!existsSync(destSrc)) {
mkdirSync(destSrc, { recursive: true });
}
const entries = readdirSync(templateSrc, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === 'pages') {
continue; // 模块模式下跳过 pages
}
const from = join(templateSrc, entry.name);
const to = join(destSrc, entry.name);
copyFolder(from, to);
}
copyFolder(join(emptyTemplatePath, 'typings'), join(rootPath, 'typings')); copyFolder(join(emptyTemplatePath, 'typings'), join(rootPath, 'typings'));
} }
else { else {
@ -356,8 +370,24 @@ export async function create(dirName: string, cmd: any) {
tsConfigWebJson, tsConfigWebJson,
checkFileExistsAndCreateType.FILE checkFileExistsAndCreateType.FILE
); );
// 更新configuration/compiler.js // 复制.gitignore
updateCompilerJsContent(rootPath, deps); const gitignoreContent = readFile(join(__dirname, '..', 'template', '.gitignore'));
checkFileExistsAndCreate(
join(rootPath, '.gitignore'),
gitignoreContent,
checkFileExistsAndCreateType.FILE
);
// 复制oak.config.json
const oakConfigContent = readFile(join(__dirname, '..', 'template', USER_CONFIG_FILE_NAME));
checkFileExistsAndCreate(
join(rootPath, USER_CONFIG_FILE_NAME),
oakConfigContent,
checkFileExistsAndCreateType.FILE
);
// 更新configuration/compiler.js (仅在非模块化模式下)
if (!isModule) {
updateCompilerJsContent(rootPath, deps);
}
Success( Success(
`${success( `${success(
`Successfully created project ${primary( `Successfully created project ${primary(
@ -390,9 +420,25 @@ export async function create(dirName: string, cmd: any) {
checkFileExistsAndCreateType.FILE checkFileExistsAndCreateType.FILE
); );
renameProject(rootPath, name, title, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_TITLE); // 只在非模块化模式下重命名整个项目包括web、wechatMp等
if (!isModule) {
renameProject(rootPath, name, title, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_TITLE);
} else {
// 模块化模式下只更新 package.json 的 name
const packageJsonFilePath = join(rootPath, 'package.json');
const packageJsonContent = readFileSync(packageJsonFilePath, 'utf-8');
const packageJsonJson = JSON.parse(packageJsonContent);
packageJsonJson.name = name;
const newPackageJsonContent = JSON.stringify(packageJsonJson, undefined, 4);
writeFileSync(packageJsonFilePath, newPackageJsonContent);
Success(
`${success(
`Change project name to ${primary(name)}`
)}`
);
}
if (example) { if (example && !isModule) {
// todo: copy template example files // todo: copy template example files
copyFolder(examplePath, rootPath, true); copyFolder(examplePath, rootPath, true);
} }

View File

@ -1,4 +1,4 @@
import { readdirSync, statSync, writeFileSync, PathLike, existsSync, unlinkSync, mkdirSync, rmdirSync, createReadStream, accessSync, createWriteStream, constants, readFileSync } from 'fs' import { readdirSync, statSync, writeFileSync, PathLike, existsSync, unlinkSync, mkdirSync, rmdirSync, createReadStream, accessSync, createWriteStream, constants, readFileSync, copyFileSync } from 'fs'
import { join } from 'path' import { join } from 'path'
import { checkFileExistsAndCreateType } from './enum' import { checkFileExistsAndCreateType } from './enum'
import { Error, error, Warn, warn } from './tip-style' import { Error, error, Warn, warn } from './tip-style'
@ -141,9 +141,8 @@ export function copyFolder(currentDir: PathLike, targetDir: PathLike, overwrite:
} }
else { else {
if (file.isFile()) { if (file.isFile()) {
const readStream = createReadStream(copyCurrentFileInfo); // 使用同步复制确保文件完全写入
const writeStream = createWriteStream(copyTargetFileInfo); copyFileSync(copyCurrentFileInfo, copyTargetFileInfo);
readStream.pipe(writeStream);
// console.log(`复制文件: ${copyCurrentFileInfo} -> ${copyTargetFileInfo}`); // console.log(`复制文件: ${copyCurrentFileInfo} -> ${copyTargetFileInfo}`);
} else { } else {
try { try {

View File

@ -5,11 +5,11 @@ import PathLib, { join } from 'path';
import Koa from 'koa'; import Koa from 'koa';
import KoaRouter from 'koa-router'; import KoaRouter from 'koa-router';
import KoaBody from 'koa-body'; import KoaBody from 'koa-body';
import logger from 'koa-logger'; // import logger from 'koa-logger';
import { AppLoader, getClusterInfo, ClusterAppLoader } from 'oak-backend-base'; import { AppLoader, getClusterInfo, ClusterAppLoader } from 'oak-backend-base';
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext'; import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
import { OakException, Connector, EntityDict, ClusterInfo } from 'oak-domain/lib/types'; import { OakException, Connector, EntityDict, ClusterInfo, OpRecord, isOakException } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { AsyncRowStore, AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncRowStore, AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
@ -25,8 +25,6 @@ import mount from 'koa-mount';
import chalk from 'chalk'; import chalk from 'chalk';
import { checkNodeVersion, randomString } from '../utils'; import { checkNodeVersion, randomString } from '../utils';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { LogFormatter, polyfillConsole, removePolyfill } from './polyfill';
import { ErrorHandler } from '../types';
checkNodeVersion() checkNodeVersion()
@ -57,18 +55,18 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
routine?: (context: AsyncContext<ED>) => Promise<void>, routine?: (context: AsyncContext<ED>) => Promise<void>,
): Promise<(() => Promise<any>) | any> { ): Promise<(() => Promise<any>) | any> {
let errorHandler: ErrorHandler<ED> | undefined = undefined; // let errorHandler: InternalErrorHandler<ED, Cxt> | undefined = undefined;
try { // try {
errorHandler = require(join( // errorHandler = require(join(
path, // path,
'lib', // 'lib',
'configuration', // 'configuration',
'errors' // 'exception'
)).default; // )).default;
} catch (err) { // } catch (err) {
// 不存在errors配置 // // 不存在exception配置
} // }
const serverConfiguration: ServerConfiguration = require(join( const serverConfiguration: ServerConfiguration = require(join(
path, path,
@ -89,7 +87,8 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
const koa = new Koa(); const koa = new Koa();
// 使用 koa-logger 中间件打印日志 // 使用 koa-logger 中间件打印日志
koa.use(logger()); // koa.use(logger());
// socket // socket
const httpServer = createServer(koa.callback()); const httpServer = createServer(koa.callback());
const socketPath = connector.getSocketPath(); const socketPath = connector.getSocketPath();
@ -216,22 +215,26 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
if (routine) { if (routine) {
// 如果传入了routine执行完成后就结束 // 如果传入了routine执行完成后就结束
const result = await appLoader.execRoutine(routine); const result = await appLoader.execRoutine(routine);
await appLoader.unmount(); // await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
return result; return result;
} }
if (errorHandler && typeof errorHandler === 'function') { // if (errorHandler && typeof errorHandler === 'function') {
polyfillConsole("startup", true, (props) => { // // polyfillConsole("startup", true, (props) => {
if (props.level === "error") { // // if (props.level === "error") {
appLoader.execRoutine(async (ctx) => { // // appLoader.execRoutine(async (ctx) => {
await errorHandler(props.caller, props.args, ctx); // // await errorHandler(props.caller, props.args, ctx);
}).catch((err) => { // // }).catch((err) => {
console.warn('执行全局错误处理失败:', err); // // console.warn('执行全局错误处理失败:', err);
}); // // });
} // // }
return props.args; // // return props.args;
}); // // });
} // // appLoader.registerInternalErrorHandler(async (ctx, type, msg, err) => {
// // await errorHandler(ctx, type, msg, err);
// // });
// }
appLoader.regAllExceptionHandler()
// 否则启动服务器模式 // 否则启动服务器模式
koa.use(async (ctx, next) => { koa.use(async (ctx, next) => {
@ -241,26 +244,49 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
console.error(err); console.error(err);
const { request } = ctx; const { request } = ctx;
const exception = const exception =
err instanceof OakException isOakException(err)
? err ? err
: new OakException<ED>( : new OakException<ED>(
serverConfiguration?.internalExceptionMask || serverConfiguration?.internalExceptionMask ||
ExceptionMask ExceptionMask
); );
const { body } = connector.serializeException( const { body, headers } = connector.serializeException(
exception, exception,
request.headers, request.headers,
request.body request.body
); );
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key])
})
return; return;
} }
}); });
koa.use( koa.use(
KoaBody(Object.assign({ KoaBody(Object.assign({
multipart: true, multipart: true,
}, serverConfiguration.koaBody)) }, serverConfiguration.koaBody))
); );
// 注册自定义中间件
if (serverConfiguration.middleware) {
if (Array.isArray(serverConfiguration.middleware)) {
serverConfiguration.middleware.forEach((mw) => {
koa.use(mw);
});
}
else if (typeof serverConfiguration.middleware === 'function') {
const mws = serverConfiguration.middleware(koa);
if (!Array.isArray(mws)) {
throw new Error('middleware 配置函数必须返回 Koa.Middleware 数组');
}
mws.forEach((mw) => {
koa.use(mw);
});
}
}
const router = new KoaRouter(); const router = new KoaRouter();
// 如果是开发环境允许options // 如果是开发环境允许options
@ -269,6 +295,10 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader())); ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
ctx.set('Access-Control-Allow-Methods', corsMethods); ctx.set('Access-Control-Allow-Methods', corsMethods);
if (connector.getCorsExposeHeaders) {
const exposeHeaders = connector.getCorsExposeHeaders();
ctx.set('Access-Control-Expose-Headers', exposeHeaders);
}
if (ctx.method == 'OPTIONS') { if (ctx.method == 'OPTIONS') {
ctx.body = 200; ctx.body = 200;
} else { } else {
@ -307,20 +337,48 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
}); });
} }
const connectorCustomAspects = connector.registerCustomAspect ? connector.registerCustomAspect() : null;
router.post(connector.getRouter(), async (ctx) => { router.post(connector.getRouter(), async (ctx) => {
const { request } = ctx; const { request } = ctx;
const { contextString, aspectName, data } = connector.parseRequest( const { contextString, aspectName, data } = await connector.parseRequest(
request.headers, request.headers,
request.body, request.body,
request.files request.files
); );
const { result, opRecords, message } = await appLoader.execAspect( let result: any;
aspectName, let opRecords: OpRecord<ED>[] = [];
request.headers, let message: string | undefined = undefined;
contextString,
data if (connectorCustomAspects &&
); connectorCustomAspects.findIndex(a => a.name === aspectName) >= 0
) {
// 自定义aspect处理
console.log(`调用Connector自定义Aspect: ${aspectName}`);
const aspect = connectorCustomAspects!.find(a => a.name === aspectName)!;
const res = await aspect.handler(
{
headers: request.headers,
contextString,
params: data,
}
);
result = res.result;
opRecords = res.opRecords || [];
message = res.message;
} else {
const res = await appLoader.execAspect(
aspectName,
request.headers,
contextString,
data
);
result = res.result;
opRecords = res.opRecords || [];
message = res.message;
}
const { body, headers } = await connector.serializeResult( const { body, headers } = await connector.serializeResult(
result, result,
opRecords, opRecords,
@ -329,6 +387,12 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
message message
); );
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key])
})
return; return;
}); });
@ -471,18 +535,70 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
appLoader.startTimers(); appLoader.startTimers();
} }
process.on('SIGINT', async () => { let isShutingdown = false;
await appLoader.unmount(); const shutdown = async () => {
process.exit(0); if (isShutingdown) {
return;
}
isShutingdown = true;
console.log('服务器正在关闭中...');
try {
await httpServer.close();
await koa.removeAllListeners();
await appLoader.execStopRoutines();
await appLoader.unmount();
} catch (err) {
console.error('关闭服务器时出错:', err);
}
}
// 处理优雅关闭的统一入口
let shutdownStarted = false;
const handleShutdown = async (signal: string) => {
// 防止重复处理
if (shutdownStarted) {
console.warn('关闭流程已启动,忽略此信号');
return;
}
shutdownStarted = true;
console.log(`\n收到 ${signal} 信号,准备关闭服务器...`);
// 移除所有信号处理器,防止重复触发
process.removeAllListeners('SIGINT');
process.removeAllListeners('SIGTERM');
process.removeAllListeners('SIGQUIT');
process.removeAllListeners('SIGHUP');
try {
await shutdown();
process.exit(0);
} catch (err) {
console.error('关闭过程出错:', err);
process.exit(1);
}
};
// 监听终止信号进行优雅关闭
// SIGINT - Ctrl+C 发送的中断信号
process.on('SIGINT', () => {
handleShutdown('SIGINT');
}); });
const shutdown = async () => { // SIGTERM - 系统/容器管理器发送的终止信号Docker、K8s、PM2等
await httpServer.close(); process.on('SIGTERM', () => {
await koa.removeAllListeners(); handleShutdown('SIGTERM');
await appLoader.unmount(); });
removePolyfill("startup"); // SIGQUIT - Ctrl+\ 发送的退出信号
} process.on('SIGQUIT', () => {
handleShutdown('SIGQUIT');
});
// SIGHUP - 终端关闭时发送(可选)
process.on('SIGHUP', () => {
handleShutdown('SIGHUP');
});
return shutdown return shutdown
} }

211
src/server/timer-manager.ts Normal file
View File

@ -0,0 +1,211 @@
// timer-manager.ts
type TimerType = 'timeout' | 'interval' | 'immediate';
interface TimerRecord {
id: NodeJS.Timeout | NodeJS.Immediate;
type: TimerType;
createdAt: number;
}
class GlobalTimerManager {
private timers = new Map<NodeJS.Timeout | NodeJS.Immediate, TimerRecord>();
private isHooked = false;
// 保存原始函数
private readonly original = {
setTimeout: global.setTimeout,
setInterval: global.setInterval,
setImmediate: global.setImmediate,clearTimeout: global.clearTimeout,
clearInterval: global.clearInterval,
clearImmediate: global.clearImmediate,};
/**
*
*/
hook(): void {
if (this.isHooked) {
console.warn('[TimerManager] Already hooked');
return;
}
const self = this;
// Hook setTimeout
global.setTimeout = function (
callback: (...args: any[]) => void,
ms?: number,
...args: any[]
): any {
const id = self.original.setTimeout(
(...callbackArgs: any[]) => {
self.timers.delete(id); // 执行完后移除
callback(...callbackArgs);
},
ms,
...args
);
self.timers.set(id, {
id,
type: 'timeout',
createdAt: Date.now(),
});
return id;
} as any;
// Hook setInterval
global.setInterval = function (
callback: (...args: any[]) => void,
ms?: number,
...args: any[]
): any {
const id = self.original.setInterval(callback, ms, ...args);
self.timers.set(id, {
id,
type: 'interval',
createdAt: Date.now(),
});
return id;
} as any;
// Hook setImmediate
global.setImmediate = function (
callback: (...args: any[]) => void,
...args: any[]
): any {
const id = self.original.setImmediate(
(...callbackArgs: any[]) => {
self.timers.delete(id); // 执行完后移除
callback(...callbackArgs);
},
...args
);
self.timers.set(id, {
id,
type: 'immediate',
createdAt: Date.now(),
});
return id;
} as any;
// Hook clear方法确保从追踪中移除
global.clearTimeout = ((id: any) => {
self.timers.delete(id);
return self.original.clearTimeout(id);
}) as any;
global.clearInterval = ((id: any) => {
self.timers.delete(id);
return self.original.clearInterval(id);
}) as any;
global.clearImmediate = ((id: any) => {
self.timers.delete(id);
return self.original.clearImmediate(id);
}) as any;
this.isHooked = true;
console.log('[TimerManager] Hooked global timer functions');
}
/**
*
*/
unhook(): void {
if (!this.isHooked) {
return;
}
global.setTimeout = this.original.setTimeout;
global.setInterval = this.original.setInterval;
global.setImmediate = this.original.setImmediate;
global.clearTimeout = this.original.clearTimeout;
global.clearInterval = this.original.clearInterval;
global.clearImmediate = this.original.clearImmediate;
this.isHooked = false;
}
/**
*
*/
clearAll(): number {
const count = this.timers.size;
this.timers.forEach((record) => {
if (record.type === 'immediate') {
this.original.clearImmediate.call(null, record.id as NodeJS.Immediate);
} else {
this.original.clearTimeout.call(null, record.id as NodeJS.Timeout);
}
});
this.timers.clear();
return count;
}
/**
*
*/
clearByType(type: TimerType): number {
let count = 0;
this.timers.forEach((record, id) => {
if (record.type === type) {
if (type === 'immediate') {
this.original.clearImmediate.call(null, id as NodeJS.Immediate);
} else {
this.original.clearTimeout.call(null, id as NodeJS.Timeout);
}
this.timers.delete(id);
count++;
}
});
return count;
}
/**
*
*/
getActiveCount(): number {
return this.timers.size;
}
/**
*
*/
getStats(): Record<TimerType, number> {
const stats: Record<TimerType, number> = {
timeout: 0,
interval: 0,
immediate: 0,
};
this.timers.forEach((record) => {
stats[record.type]++;
});
return stats;
}
/**
*
*/
getTimers(): TimerRecord[] {
return Array.from(this.timers.values());
}
}
export const timerManager = new GlobalTimerManager();
export const hookTimers = () => timerManager.hook();
export const unhookTimers = () => timerManager.unhook();
export const clearAllTimers = () => timerManager.clearAll();
export const getTimerStats = () => timerManager.getStats();

View File

@ -3,9 +3,11 @@ import ts, { CompilerOptions, ProjectReference } from "typescript";
import pathLib from "path"; import pathLib from "path";
import dayjs from "dayjs"; import dayjs from "dayjs";
import fs from "fs"; import fs from "fs";
import { cloneDeep } from "lodash"; // import { cloneDeep } from "lodash";
import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore"; import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore";
import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill"; import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill";
import { prepareSingleFileReplaceTscAliasPaths, SingleFileReplacer } from 'tsc-alias';
import { clearAllTimers, hookTimers } from "./timer-manager";
/* /*
* *
@ -397,15 +399,23 @@ const createServerManager = (
console.warn("----> Starting service......"); console.warn("----> Starting service......");
// 这里注意要在require之前因为require会触发编译 try {
const { startup } = require('./start') as { clearAllTimers();
startup: (pwd: string, connector: any) => Promise<() => Promise<void>>; // 这里注意要在require之前因为require会触发编译
} const { startup } = require('./start') as {
startup: (pwd: string, connector: any) => Promise<() => Promise<void>>;
}
shutdown = await startup(pwd, simpleConnector).then((shutdown) => { shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
config.lifecycle.onServerStart(config); config.lifecycle.onServerStart(config);
return shutdown; return shutdown;
}); });
} catch (error) {
console.error("----> Failed to start service:", error);
isRestarting = false;
return;
}
isRestarting = false; isRestarting = false;
await eventEmitter.emit("server-restarted", {}); await eventEmitter.emit("server-restarted", {});
@ -425,13 +435,14 @@ const createServerManager = (
}; };
// 创建编译器 // 创建编译器
const createCompiler = ( const createCompiler = async (
projectPath: string, projectPath: string,
options: CompilerOptions, options: CompilerOptions,
projectReferences: readonly ProjectReference[] | undefined, projectReferences: readonly ProjectReference[] | undefined,
aliasConfig: Record<string, string | string[]>, treatFile: (filePath: string) => void,
config: RealWatchConfig config: RealWatchConfig
) => { ) => {
const createProgramAndSourceFile = (path: string) => { const createProgramAndSourceFile = (path: string) => {
const program = ts.createProgram({ const program = ts.createProgram({
rootNames: [path], rootNames: [path],
@ -468,11 +479,12 @@ const createCompiler = (
const compileTask = async (task: CompileTask): Promise<CompileResult> => { const compileTask = async (task: CompileTask): Promise<CompileResult> => {
const { filePath, changeType } = task; const { filePath, changeType } = task;
const modulePath = pathLib.resolve(filePath);
// 判断文件类型 // 判断文件类型
if (!filePath.endsWith(".ts")) { if (!filePath.endsWith(".ts")) {
// 处理非TypeScript文件 (如JSON文件) // 处理非TypeScript文件 (如JSON文件)
if (filePath.endsWith(".json")) { if (filePath.endsWith(".json")) {
const targetPath = filePath.replace( const targetPath = modulePath.replace(
pathLib.join(projectPath, "src"), pathLib.join(projectPath, "src"),
pathLib.join(projectPath, "lib") pathLib.join(projectPath, "lib")
); );
@ -524,7 +536,6 @@ const createCompiler = (
console.clear(); console.clear();
console.warn(`File ${filePath} has been ${changeType}d`); console.warn(`File ${filePath} has been ${changeType}d`);
const modulePath = pathLib.resolve(filePath);
const libPath = modulePath const libPath = modulePath
.replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib")) .replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib"))
.replace(/\.ts$/, ".js"); .replace(/\.ts$/, ".js");
@ -578,6 +589,8 @@ const createCompiler = (
} else { } else {
console.log(`Emit succeeded for ${filePath}.`); console.log(`Emit succeeded for ${filePath}.`);
config.lifecycle.onAfterCompile(config); config.lifecycle.onAfterCompile(config);
const jsFilePath = libPath;
treatFile(jsFilePath);
return { return {
taskId: task.id, taskId: task.id,
success: true, success: true,
@ -666,42 +679,42 @@ const generateTaskId = (): string => {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}; };
/** // /**
* alias // * 根据 alias 配置表将路径中的别名替换为真实路径
* @param path - "@project/file" "@oak-app-domain/some-module" // * @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
* @param aliasConfig - alias key value // * @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组
* @returns alias // * @returns 替换后的路径,如果没有匹配到 alias则返回原始路径
*/ // */
const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => { // const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => {
for (const [alias, targets] of Object.entries(aliasConfig)) { // for (const [alias, targets] of Object.entries(aliasConfig)) {
// If alias ends with "*", handle it as a dynamic alias. // // If alias ends with "*", handle it as a dynamic alias.
if (alias.endsWith('*')) { // if (alias.endsWith('*')) {
// Create a regex pattern that matches paths starting with the alias, followed by any characters // // Create a regex pattern that matches paths starting with the alias, followed by any characters
const aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)' // const aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)'
const match = path.match(aliasPattern); // const match = path.match(aliasPattern);
if (match) { // if (match) {
// Replace the alias with the target path, appending the matched part from the original path // // Replace the alias with the target path, appending the matched part from the original path
const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array) // const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
// Ensure that the target path ends with a slash if it's not already // // Ensure that the target path ends with a slash if it's not already
const replacedPath = target.replace(/\/\*$/, "/") + match[1]; // const replacedPath = target.replace(/\/\*$/, "/") + match[1];
return replacedPath; // return replacedPath;
} // }
} else { // } else {
// Handle static alias without "*" by directly matching the path // // Handle static alias without "*" by directly matching the path
if (path.startsWith(alias)) { // if (path.startsWith(alias)) {
const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array) // const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
// Replace the alias part with the target path // // Replace the alias part with the target path
return path.replace(alias, target); // return path.replace(alias, target);
} // }
} // }
} // }
// If no alias matches, return the original path // // If no alias matches, return the original path
return path; // return path;
}; // };
export const watch = ( export const watch = async (
projectPath: string, projectPath: string,
config?: WatchConfig config?: WatchConfig
): Promise<() => Promise<void>> => { ): Promise<() => Promise<void>> => {
@ -719,29 +732,42 @@ export const watch = (
throw new Error("Could not find a valid 'tsconfig.build.json'."); throw new Error("Could not find a valid 'tsconfig.build.json'.");
} }
// 读取配置文件 const runFile: SingleFileReplacer = await prepareSingleFileReplaceTscAliasPaths({
configFile: pathLib.join(projectPath, "tsconfig.build.json"),
resolveFullPaths: true,
});
function treatFile(filePath: string) {
console.log(`Processing file for alias replacement: ${filePath}`);
const fileContents = fs.readFileSync(filePath, 'utf8');
const newContents = runFile({ fileContents, filePath });
// do stuff with newContents
fs.writeFileSync(filePath, newContents, 'utf8');
}
// // 读取配置文件
const configFile = ts.readConfigFile(configFileName, ts.sys.readFile); const configFile = ts.readConfigFile(configFileName, ts.sys.readFile);
// 解析配置文件内容 // // 解析配置文件内容
const { options, projectReferences } = ts.parseJsonConfigFileContent( const { options, projectReferences } = ts.parseJsonConfigFileContent(
configFile.config, configFile.config,
ts.sys, ts.sys,
pathLib.dirname(configFileName) pathLib.dirname(configFileName)
); );
const aliasConfig: AliasConfig = cloneDeep(options.paths) || {}; // const aliasConfig: AliasConfig = cloneDeep(options.paths) || {};
// 输出原始配置 // // 输出原始配置
// console.log("[DEBUG] Original alias config:", aliasConfig); // // console.log("[DEBUG] Original alias config:", aliasConfig);
Object.keys(aliasConfig).forEach((key) => { // Object.keys(aliasConfig).forEach((key) => {
const value = aliasConfig[key]; // const value = aliasConfig[key];
// 替换src // // 替换src
aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v: string) => v.replace("src", "lib")); // aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v: string) => v.replace("src", "lib"));
}); // });
// 输出真实的alias配置 // 输出真实的alias配置
console.debug("[DEBUG] Running Alias config:", aliasConfig); // console.debug("[DEBUG] Running Alias config:", aliasConfig);
// 初始化polyfill // 初始化polyfill
const polyfillLoader = () => { const polyfillLoader = () => {
@ -781,6 +807,8 @@ export const watch = (
if (emitResult.emitSkipped) { if (emitResult.emitSkipped) {
console.error(`[resolve] Emit skipped for: ${tsPath}`); console.error(`[resolve] Emit skipped for: ${tsPath}`);
throw new Error("TypeScript emit skipped"); throw new Error("TypeScript emit skipped");
} else {
treatFile(jsPath)
} }
console.log(`[resolve] Successfully compiled: ${tsPath}`); console.log(`[resolve] Successfully compiled: ${tsPath}`);
@ -823,13 +851,12 @@ export const watch = (
rFoptions: object | undefined // 解析选项 rFoptions: object | undefined // 解析选项
) { ) {
let resolvedRequest = request; let resolvedRequest = request;
const replacedPath = replaceAliasWithPath(request, aliasConfig); // const replacedPath = replaceAliasWithPath(request, aliasConfig);
if (replacedPath !== request) {
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
resolvedRequest = pathLib.join(projectPath, replacedPath);
}
// if (replacedPath !== request) {
// console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
// resolvedRequest = pathLib.join(projectPath, replacedPath);
// }
try { try {
return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions); return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
} catch (error: any) { } catch (error: any) {
@ -854,6 +881,9 @@ export const watch = (
polyfillLoader(); polyfillLoader();
realConfig.polyfill.console.enable && polyfillConsole("watch", enableTrace, realConfig.polyfill?.console?.formatter); realConfig.polyfill.console.enable && polyfillConsole("watch", enableTrace, realConfig.polyfill?.console?.formatter);
// 监听定时器相关的API,确保定时器在重启后不会重复触发
hookTimers()
// 初始编译检查 // 初始编译检查
const initialCompile = () => { const initialCompile = () => {
const serverConfigFile = pathLib.join(projectPath, "lib/configuration/server.js"); const serverConfigFile = pathLib.join(projectPath, "lib/configuration/server.js");
@ -908,16 +938,36 @@ export const watch = (
"src/ports/index.ts", "src/ports/index.ts",
]; ];
compileFiles.forEach(tryCompile); compileFiles.forEach(tryCompile);
// 最后替换lib目录下所有的路径别名
console.log(`[watch] Replacing path aliases in lib directory......`);
const libDir = pathLib.join(projectPath, "lib");
const walkDir = (dir: string) => {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const fullPath = pathLib.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walkDir(fullPath);
}
else if (stat.isFile() && fullPath.endsWith(".js")) {
const fileContents = fs.readFileSync(fullPath, 'utf8');
const newContents = runFile({ fileContents, filePath: fullPath });
fs.writeFileSync(fullPath, newContents, 'utf8');
}
});
};
walkDir(libDir);
console.log(`[watch] Path alias replacement completed.`);
} }
}; };
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
// 创建事件系统 // 创建事件系统
const eventEmitter = createEventEmitter(); const eventEmitter = createEventEmitter();
// 创建各个组件 // 创建各个组件
const compileQueue = createCompileQueue(eventEmitter); const compileQueue = createCompileQueue(eventEmitter);
const compiler = createCompiler(projectPath, options, projectReferences, aliasConfig, realConfig); const compiler = await createCompiler(projectPath, options, projectReferences, treatFile, realConfig);
const serverManager = createServerManager(projectPath, eventEmitter, realConfig); const serverManager = createServerManager(projectPath, eventEmitter, realConfig);
const fileWatcher = createFileWatcher(projectPath, eventEmitter); const fileWatcher = createFileWatcher(projectPath, eventEmitter);

View File

@ -92,7 +92,7 @@ export function packageJsonContent({
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze", "build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
"build:mp": "${cliBinName} build --target mp --mode production", "build:mp": "${cliBinName} build --target mp --mode production",
"build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze", "build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze",
"build:watch": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch", "build:watch": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch",
"start:web": "${cliBinName} start --target web --mode development --devMode frontend", "start:web": "${cliBinName} start --target web --mode development --devMode frontend",
"start:web:server": "${cliBinName} start --target web --mode development", "start:web:server": "${cliBinName} start --target web --mode development",
"start:native": "${cliBinName} start --target rn --mode development --devMode frontend", "start:native": "${cliBinName} start --target rn --mode development --devMode frontend",
@ -104,7 +104,7 @@ export function packageJsonContent({
"build-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap", "build-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap",
"build-analyze:web": "${cliBinName} build --target web --mode production --analyze", "build-analyze:web": "${cliBinName} build --target web --mode production --analyze",
"build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --analyze", "build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --analyze",
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json", "build": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json",
"prebuild": "npm run make:locale", "prebuild": "npm run make:locale",
"run:ios": "oak-cli run -p ios", "run:ios": "oak-cli run -p ios",
"run:android": "oak-cli run -p android", "run:android": "oak-cli run -p android",
@ -406,7 +406,13 @@ export function tsConfigBuildJsonContent() {
"test", "test",
"src/pages/**/*", "src/pages/**/*",
"src/components/**/*" "src/components/**/*"
] ],
"oakBuildChecks": {
"context": {
"checkAsyncContext": true,
"targetModules": ["context/BackendRuntimeContext"]
}
}
}`; }`;
} }
@ -436,7 +442,7 @@ export function tsConfigPathsJsonContent(deps: string[]) {
compilerOptions: { compilerOptions: {
baseUrl: "./", baseUrl: "./",
paths, paths,
typeRoots: ["./typings"] typeRoots: ["./typings", "node_modules/@types"]
} }
}, null, '\t'); }, null, '\t');
} }
@ -663,9 +669,14 @@ export function oakConfigContentWithWeb() {
export function updateCompilerJsContent(directory: string, deps: string[]) { export function updateCompilerJsContent(directory: string, deps: string[]) {
const compilerJsPath = join(directory, 'configuration', 'compiler.js'); const compilerJsPath = join(directory, 'configuration', 'compiler.js');
assert(existsSync(compilerJsPath));
// 只有在有依赖项时才需要修改 compiler.js
if (deps.length > 0) { if (deps.length > 0) {
// 检查文件是否存在
if (!existsSync(compilerJsPath)) {
console.warn(`Warning: ${compilerJsPath} does not exist, skipping compiler.js update`);
return;
}
const { ast } = transformFileSync(compilerJsPath, { ast: true })!; const { ast } = transformFileSync(compilerJsPath, { ast: true })!;
const { program } = ast!; const { program } = ast!;
const { body } = program!; const { body } = program!;

View File

@ -1,5 +0,0 @@
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<ED extends EntityDict & BaseEntityDict> = ( caller: NodeJS.CallSite | null, args: any[], ctx: AsyncContext<ED>) => Promise<void>;

View File

@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const { build } = require('oak-domain/lib/compiler/tscBuilder.js')
const pwd = process.cwd();
build(pwd, process.argv);

View File

@ -0,0 +1,26 @@
import { Routine } from 'oak-domain/lib/types/Timer';
import { EntityDict } from '@oak-app-domain';
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
// process.on('uncaughtException', (err) => {
// console.error(`Caught exception: ${err}`);
// // Optionally, you can exit the process or perform cleanup
// });
// process.on('unhandledRejection', (err) => {
// console.error(`Caught rejection: ${err}`);
// // Optionally, you can exit the process or perform cleanup
// });
const stopRoutines: Array<Routine<EntityDict, keyof EntityDict, BackendRuntimeContext>> = [
{
name: '示例性routine_stop',
routine: async (context, env) => {
console.log('示例性routine执行请在src/routine/stop.ts中关闭');
return context.opResult;
},
},
];
export default stopRoutines;