Compare commits

...

51 Commits
4.0.25 ... 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
Xu Chang af34753f48 4.0.28-pub 2025-10-16 15:10:41 +08:00
Xu Chang 6bd83ff6e7 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-cli into dev 2025-10-16 15:00:52 +08:00
Pan Qiancheng 97bc9b8042 fix: 修改一处已弃用的类型 2025-10-16 11:21:43 +08:00
Pan Qiancheng df8426d102 fix: 处理require errors.js不存在的情况 2025-10-16 11:18:42 +08:00
Xu Chang 04c53f956a Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-cli into dev 2025-10-16 11:11:34 +08:00
Xu Chang 0b833b8fd6 4.0.28 2025-10-16 11:11:29 +08:00
Xu Chang a3e2d584a8 4.0.27-pub 2025-10-16 11:10:09 +08:00
Pan Qiancheng 8c642d79c2 fix: 添加一层catch处理 2025-10-16 11:08:33 +08:00
Xu Chang b75a783aae Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-cli into dev 2025-10-16 11:08:33 +08:00
Xu Chang 80e09e4f6f 4.0.27-dev 2025-10-16 11:08:27 +08:00
Xu Chang 523aac80c2 4.0.26-pub 2025-10-16 11:07:26 +08:00
Pan Qiancheng be24825206 build 2025-10-16 10:59:19 +08:00
Pan Qiancheng 893b1b04cb feat: 支持在configuration中新建errors.ts来对项目的console.error做额外的polyfill处理 2025-10-16 10:59:12 +08:00
Xu Chang cd7afc29a3 更新了initServer时的默认行为 2025-10-07 18:16:04 +08:00
Xu Chang a2cca9dd46 更新了initServer时的默认行为 2025-10-07 18:15:45 +08:00
Xu Chang 2bbc326934 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-cli into dev 2025-10-07 18:10:20 +08:00
Xu Chang 747c40eabe 规范了initialize时的行为 2025-10-07 18:10:15 +08:00
Xu Chang 8571458e97 template中部分页面的title修正 2025-08-26 12:23:05 +08:00
pqcqaq 61705bf9e1 feat: 完全重构并优化了server watch,通过编译链与事件机制解决多文件修改的情况,并且支持了在i18n文件更新的情况下,自动同步到数据库(利用了routine) 2025-08-18 16:21:14 +08:00
Xu Chang b3f56fb7b8 4.0.26-dev 2025-08-18 15:15:39 +08:00
42 changed files with 2526 additions and 921 deletions

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = build;
const tslib_1 = require("tslib");
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn"));
@ -184,4 +185,3 @@ async function build(cmd) {
(0, tip_style_1.Error)(`${(0, tip_style_1.error)(`target could only be web or mp(wechatMp) or rn(native)`)}`);
}
}
exports.default = build;

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = run;
const tslib_1 = require("tslib");
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn"));
@ -48,4 +49,3 @@ async function run(options) {
process.exit(-1);
}
}
exports.default = run;

View File

@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.update = exports.create = void 0;
exports.create = create;
exports.update = update;
const tslib_1 = require("tslib");
const ts = tslib_1.__importStar(require("typescript"));
const fs_1 = require("fs");
@ -190,8 +191,22 @@ async function create(dirName, cmd) {
(0, file_handle_1.checkFileExistsAndCreate)(rootPath);
// 复制项目文件
if (isModule) {
// 模块化的项目只拷贝src和typings目录
(0, file_handle_1.copyFolder)((0, path_1.join)(emptyTemplatePath, 'src'), (0, path_1.join)(rootPath, 'src'));
// 模块化的项目,只拷贝 src 下的内容,但跳过 pages 目录;同时拷贝 typings
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'));
}
else {
@ -233,8 +248,16 @@ async function create(dirName, cmd) {
(0, file_handle_1.checkFileExistsAndCreate)(tsConfigMpJsonPath, tsConfigMpJson, enum_1.checkFileExistsAndCreateType.FILE);
// 创建tsconfig.web.json
(0, file_handle_1.checkFileExistsAndCreate)(tsConfigWebJsonPath, tsConfigWebJson, enum_1.checkFileExistsAndCreateType.FILE);
// 更新configuration/compiler.js
(0, template_1.updateCompilerJsContent)(rootPath, deps);
// 复制.gitignore
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)}`)}`);
shelljs_1.default.cd(dirName);
if (deps.length > 0) {
@ -242,7 +265,7 @@ async function create(dirName, cmd) {
}
// 获取package.json内容
const packageJson = (0, template_1.packageJsonContent)({
name: DEFAULT_PROJECT_NAME,
name: DEFAULT_PROJECT_NAME, // 后面再统一rename
version,
description,
cliName: config_1.CLI_NAME,
@ -253,8 +276,21 @@ async function create(dirName, cmd) {
});
// 创建package.json
(0, file_handle_1.checkFileExistsAndCreate)(packageJsonPath, packageJson, enum_1.checkFileExistsAndCreateType.FILE);
(0, rename_1.renameProject)(rootPath, name, title, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_TITLE);
if (example) {
// 只在非模块化模式下重命名整个项目包括web、wechatMp等
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
(0, file_handle_1.copyFolder)(examplePath, rootPath, true);
}
@ -265,7 +301,6 @@ async function create(dirName, cmd) {
(0, tip_style_1.Error)((0, tip_style_1.error)(err));
}
}
exports.create = create;
async function update(dirName, subDirName, cmd) {
const isDev = cmd.dev ? true : false;
try {
@ -292,4 +327,3 @@ async function update(dirName, subDirName, cmd) {
console.error((0, tip_style_1.error)(err.message));
}
}
exports.update = update;

View File

@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CreateCompilerConfig = void 0;
exports.CreateCompilerConfig = CreateCompilerConfig;
/**
* 创建一个oak编译器配置
* @param raw 原始配置
@ -11,7 +11,6 @@ function CreateCompilerConfig(raw) {
// 在这里可以做配置的预处理
return raw;
}
exports.CreateCompilerConfig = CreateCompilerConfig;
/**
* 将compiler.js中的模块导出使用以下形式
* module.exports = CreateComilerConfig({})

View File

@ -1,5 +1,3 @@
/// <reference types="node" />
/// <reference types="node" />
import { PathLike } from 'fs';
import { checkFileExistsAndCreateType } from './enum';
/**
@ -39,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): NonSharedBuffer | undefined;
/**
* @name
* @export

View File

@ -1,6 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkFileExistsAndCreate = exports.checkFileExists = exports.copyFolder = exports.readFile = exports.writeFile = exports.deleteFolderRecursive = exports.parseJsonFile = exports.parseJsonFiles = exports.readDirGetFile = exports.readDirPath = void 0;
exports.readDirPath = readDirPath;
exports.readDirGetFile = readDirGetFile;
exports.parseJsonFiles = parseJsonFiles;
exports.parseJsonFile = parseJsonFile;
exports.deleteFolderRecursive = deleteFolderRecursive;
exports.writeFile = writeFile;
exports.readFile = readFile;
exports.copyFolder = copyFolder;
exports.checkFileExists = checkFileExists;
exports.checkFileExistsAndCreate = checkFileExistsAndCreate;
const fs_1 = require("fs");
const path_1 = require("path");
const enum_1 = require("./enum");
@ -25,7 +34,6 @@ function readDirPath(entry) {
}
return pathList;
}
exports.readDirPath = readDirPath;
/**
* @name 读取指定目录的文件不进行深度遍历只获取根目录
* @export
@ -36,7 +44,6 @@ function readDirGetFile(entry) {
const dirInfo = (0, fs_1.readdirSync)(entry);
return dirInfo;
}
exports.readDirGetFile = readDirGetFile;
/**
* @name 解析json文件(数组)
* @export
@ -51,7 +58,6 @@ function parseJsonFiles(arr) {
}
return result;
}
exports.parseJsonFiles = parseJsonFiles;
/**
* @name 解析单个文件json
* @export
@ -67,7 +73,6 @@ function parseJsonFile(file) {
return;
}
}
exports.parseJsonFile = parseJsonFile;
/**
* @name 删除文件夹
* @export
@ -97,7 +102,6 @@ function deleteFolderRecursive(entry) {
// console.log("文件夹不存在");
}
}
exports.deleteFolderRecursive = deleteFolderRecursive;
;
function writeFile(path, data) {
try {
@ -108,7 +112,6 @@ function writeFile(path, data) {
(0, tip_style_1.Error)((0, tip_style_1.error)('文件写入失败'));
}
}
exports.writeFile = writeFile;
function readFile(path, options) {
try {
const data = (0, fs_1.readFileSync)(path, options);
@ -119,7 +122,6 @@ function readFile(path, options) {
(0, tip_style_1.Error)((0, tip_style_1.error)('文件读取失败'));
}
}
exports.readFile = readFile;
/**
* @name 拷贝文件夹
* @export
@ -147,9 +149,8 @@ function copyFolder(currentDir, targetDir, overwrite = false) {
}
else {
if (file.isFile()) {
const readStream = (0, fs_1.createReadStream)(copyCurrentFileInfo);
const writeStream = (0, fs_1.createWriteStream)(copyTargetFileInfo);
readStream.pipe(writeStream);
// 使用同步复制确保文件完全写入
(0, fs_1.copyFileSync)(copyCurrentFileInfo, copyTargetFileInfo);
// console.log(`复制文件: ${copyCurrentFileInfo} -> ${copyTargetFileInfo}`);
}
else {
@ -181,7 +182,6 @@ function copyFolder(currentDir, targetDir, overwrite = false) {
throw new global.Error(`需要copy的文件夹不存在: ${currentDir}`);
}
}
exports.copyFolder = copyFolder;
/**
* @name 检测文件/文件夹是否存在
* @export
@ -191,7 +191,6 @@ exports.copyFolder = copyFolder;
function checkFileExists(path) {
return (0, fs_1.existsSync)(path);
}
exports.checkFileExists = checkFileExists;
/**
* @name 检测文件/文件夹是否存在不存在则创建
* @export
@ -217,4 +216,3 @@ function checkFileExistsAndCreate(path, data, type = enum_1.checkFileExistsAndCr
throw new global.Error(`${path} already exists!`);
}
}
exports.checkFileExistsAndCreate = checkFileExistsAndCreate;

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = make;
const tslib_1 = require("tslib");
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn"));
@ -21,4 +22,3 @@ async function make(cmd) {
process.exit(-1);
}
}
exports.default = make;

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = make;
const tslib_1 = require("tslib");
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn"));
@ -20,4 +21,3 @@ async function make() {
process.exit(-1);
}
}
exports.default = make;

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = make;
const tslib_1 = require("tslib");
const tip_style_1 = require("./tip-style");
const localeBuilder_1 = tslib_1.__importDefault(require("oak-domain/lib/compiler/localeBuilder"));
@ -18,4 +19,3 @@ async function make(cmd) {
process.exit(-1);
}
}
exports.default = make;

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = make;
const tslib_1 = require("tslib");
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn"));
@ -30,4 +31,3 @@ async function make(cmd, watch) {
process.exit(-1);
}
}
exports.default = make;

View File

@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rename = exports.renameProject = void 0;
exports.renameProject = renameProject;
exports.rename = rename;
const path_1 = require("path");
const fs_1 = require("fs");
const editTemplate_1 = require("@react-native-community/cli/build/commands/init/editTemplate");
@ -44,7 +45,6 @@ async function renameProject(dir, name, title, placeholderName, placeholderTitle
process.chdir(cwd);
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Change project name to ${(0, tip_style_1.primary)(name)}, project title to ${(0, tip_style_1.primary)(title)}`)}`);
}
exports.renameProject = renameProject;
async function rename(cmd) {
const { oldName, newName, oldTitle, newTitle } = cmd;
if (!oldName) {
@ -65,4 +65,3 @@ async function rename(cmd) {
}
renameProject(process.cwd(), newName, newTitle, oldName, oldTitle);
}
exports.rename = rename;

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = run;
const tslib_1 = require("tslib");
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn"));
@ -59,4 +60,3 @@ async function run(options) {
process.exit(-1);
}
}
exports.default = run;

View File

@ -1,5 +1,4 @@
/// <reference path="../../src/typings/polyfill.d.ts" />
import { EntityDict } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
export declare function initialize<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>>(path: string): Promise<void>;
export declare function initialize<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>>(path: string, ifExists?: 'drop' | 'omit' | 'dropIfNotStatic'): Promise<void>;

View File

@ -1,13 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialize = void 0;
exports.initialize = initialize;
/// <reference path="../typings/polyfill.d.ts" />
const oak_backend_base_1 = require("oak-backend-base");
async function initialize(path) {
async function initialize(path, ifExists) {
const appLoader = new oak_backend_base_1.AppLoader(path);
await appLoader.mount(true);
await appLoader.initialize();
await appLoader.initialize(ifExists);
await appLoader.unmount();
console.log('data initialized');
}
exports.initialize = initialize;

View File

@ -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;

View File

@ -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;

View File

@ -1,4 +1,3 @@
/// <reference path="../../src/typings/polyfill.d.ts" />
import './polyfill';
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
import { Connector, EntityDict } from 'oak-domain/lib/types';

View File

@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startup = void 0;
exports.startup = startup;
const tslib_1 = require("tslib");
/// <reference path="../typings/polyfill.d.ts" />
require("./polyfill");
@ -9,7 +9,7 @@ const path_1 = require("path");
const koa_1 = tslib_1.__importDefault(require("koa"));
const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
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 types_1 = require("oak-domain/lib/types");
const cluster_adapter_1 = require("@socket.io/cluster-adapter");
@ -38,6 +38,17 @@ function concat(...paths) {
});
}
async function startup(path, connector, omitWatchers, omitTimers, routine) {
// let errorHandler: InternalErrorHandler<ED, Cxt> | undefined = undefined;
// try {
// errorHandler = require(join(
// path,
// 'lib',
// 'configuration',
// 'exception'
// )).default;
// } catch (err) {
// // 不存在exception配置
// }
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'));
@ -51,7 +62,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
const corsMethods = ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'];
const koa = new koa_1.default();
// 使用 koa-logger 中间件打印日志
koa.use((0, koa_logger_1.default)());
// koa.use(logger());
// socket
const httpServer = (0, http_1.createServer)(koa.callback());
const socketPath = connector.getSocketPath();
@ -64,7 +75,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
}
: serverConfiguration.cors
? {
origin: serverConfiguration.cors.origin,
origin: serverConfiguration.cors.origin, //socket.io配置cors origin是支持数组和字符串
allowedHeaders: [
...corsHeaders.concat(connector.getCorsHeader()),
...(serverConfiguration.cors.headers || []),
@ -146,7 +157,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
if (!ui?.disable) {
(0, admin_ui_1.instrument)(io, {
auth: {
type: "basic",
type: "basic", // 使用基本认证,生产建议关闭或换成自定义 auth
username: ui?.username || "admin",
password: bcryptjs_1.default.hashSync(passwordForAdminUI, 10), // 必须使用 bcrypt 加密之后的密码
},
@ -161,9 +172,25 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
if (routine) {
// 如果传入了routine执行完成后就结束
const result = await appLoader.execRoutine(routine);
await appLoader.unmount();
// await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
return result;
}
// if (errorHandler && typeof errorHandler === 'function') {
// // polyfillConsole("startup", true, (props) => {
// // if (props.level === "error") {
// // appLoader.execRoutine(async (ctx) => {
// // await errorHandler(props.caller, props.args, ctx);
// // }).catch((err) => {
// // console.warn('执行全局错误处理失败:', err);
// // });
// // }
// // return props.args;
// // });
// // appLoader.registerInternalErrorHandler(async (ctx, type, msg, err) => {
// // await errorHandler(ctx, type, msg, err);
// // });
// }
appLoader.regAllExceptionHandler();
// 否则启动服务器模式
koa.use(async (ctx, next) => {
try {
@ -172,18 +199,39 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
catch (err) {
console.error(err);
const { request } = ctx;
const exception = err instanceof types_1.OakException
const exception = (0, types_1.isOakException)(err)
? err
: new types_1.OakException(serverConfiguration?.internalExceptionMask ||
ExceptionMask);
const { body } = connector.serializeException(exception, request.headers, request.body);
const { body, headers } = connector.serializeException(exception, request.headers, request.body);
ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key]);
});
return;
}
});
koa.use((0, koa_body_1.default)(Object.assign({
multipart: true,
}, 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();
// 如果是开发环境允许options
if (['development', 'staging'].includes(process.env.NODE_ENV)) {
@ -191,6 +239,10 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
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') {
ctx.body = 200;
}
@ -224,12 +276,39 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
}
});
}
const connectorCustomAspects = connector.registerCustomAspect ? connector.registerCustomAspect() : null;
router.post(connector.getRouter(), async (ctx) => {
const { request } = ctx;
const { contextString, aspectName, data } = connector.parseRequest(request.headers, request.body, request.files);
const { result, opRecords, message } = await appLoader.execAspect(aspectName, request.headers, contextString, data);
const { contextString, aspectName, data } = await connector.parseRequest(request.headers, request.body, request.files);
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);
ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key]);
});
return;
});
// 桥接访问外部资源的入口
@ -357,15 +436,63 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
if (!omitTimers) {
appLoader.startTimers();
}
process.on('SIGINT', async () => {
await appLoader.unmount();
process.exit(0);
});
let isShutingdown = false;
const shutdown = async () => {
await httpServer.close();
await koa.removeAllListeners();
await appLoader.unmount();
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) => {
// 防止重复处理
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;
}
exports.startup = startup;

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;

46
lib/server/watch.d.ts vendored
View File

@ -1,11 +1,6 @@
/// <reference types="node" />
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;
/**
* polyfill
*/
@ -36,34 +31,59 @@ export type WatchConfig = {
*
* @returns void
*/
onInit?: () => void;
onInit?: (config: RealWatchConfig) => void;
/**
*
* @returns void
*/
onServerStart?: () => void;
onServerStart?: (config: RealWatchConfig) => void;
/**
*
* @returns void
*/
onBeforeCompile?: () => void;
onBeforeCompile?: (config: RealWatchConfig) => void;
/**
*
* @returns void
*/
onAfterCompile?: () => void;
onAfterCompile?: (config: RealWatchConfig) => void;
/**
*
* @returns void
*/
onServerShutdown?: () => void;
onServerShutdown?: (config: RealWatchConfig) => void;
/**
*
* @returns void
*/
onDispose?: () => void;
onDispose?: (config: RealWatchConfig) => void;
};
};
export type FileChangeType = "add" | "remove" | "change";
export type FileChangeEvent = {
path: string;
type: FileChangeType;
timestamp: number;
};
export type CompileTask = {
id: string;
filePath: string;
changeType: FileChangeType;
timestamp: number;
};
export type CompileResult = {
taskId: string;
success: boolean;
filePath: string;
error?: string;
};
export type EventType = "file-changed" | "compile-task-added" | "compile-task-completed" | "compile-batch-started" | "compile-batch-completed" | "server-restart-needed" | "server-restarted";
export type EventHandler<T = any> = (data: T) => void | Promise<void>;
export type EventEmitter = {
on: <T>(event: EventType, handler: EventHandler<T>) => void;
emit: <T>(event: EventType, data: T) => Promise<void>;
off: <T>(event: EventType, handler: EventHandler<T>) => void;
};
type DeepRequiredIfPresent<T> = {
[P in keyof T]-?: NonNullable<T[P]> extends (...args: any) => any ? T[P] : NonNullable<T[P]> extends (infer U)[] ? DeepRequiredIfPresent<U>[] : NonNullable<T[P]> extends object ? DeepRequiredIfPresent<NonNullable<T[P]>> : T[P] extends undefined | null ? T[P] : NonNullable<T[P]>;
};

View File

@ -7,8 +7,108 @@ const typescript_1 = tslib_1.__importDefault(require("typescript"));
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 tsc_alias_1 = require("tsc-alias");
const timer_manager_1 = require("./timer-manager");
// 创建事件发射器
const createEventEmitter = () => {
const listeners = new Map();
return {
on: (event, handler) => {
if (!listeners.has(event)) {
listeners.set(event, new Set());
}
listeners.get(event).add(handler);
},
emit: async (event, data) => {
const handlers = listeners.get(event);
if (handlers) {
const promises = Array.from(handlers).map(handler => Promise.resolve(handler(data)));
await Promise.all(promises);
}
},
off: (event, handler) => {
const handlers = listeners.get(event);
if (handlers) {
handlers.delete(handler);
}
}
};
};
// 创建编译任务队列
const createCompileQueue = (eventEmitter) => {
const queue = [];
let isProcessing = false;
let processingCount = 0;
let taskProcessor = async (task) => ({
taskId: task.id,
success: true,
filePath: task.filePath
});
const addTask = (task) => {
// 检查是否有相同文件的任务已存在,如果有则更新时间戳
const existingIndex = queue.findIndex(t => t.filePath === task.filePath);
if (existingIndex !== -1) {
queue[existingIndex] = { ...queue[existingIndex], ...task };
}
else {
queue.push(task);
}
eventEmitter.emit("compile-task-added", task);
processQueue();
};
const processQueue = async () => {
if (isProcessing || queue.length === 0) {
return;
}
isProcessing = true;
processingCount = queue.length;
await eventEmitter.emit("compile-batch-started", { count: processingCount });
const tasksToProcess = [...queue];
queue.length = 0; // 清空队列
const results = [];
for (const task of tasksToProcess) {
try {
const result = await taskProcessor(task);
results.push(result);
await eventEmitter.emit("compile-task-completed", result);
}
catch (error) {
const result = {
taskId: task.id,
success: false,
filePath: task.filePath,
error: error instanceof Error ? error.message : String(error)
};
results.push(result);
await eventEmitter.emit("compile-task-completed", result);
}
}
isProcessing = false;
processingCount = 0;
await eventEmitter.emit("compile-batch-completed", { results });
// 检查是否需要重启服务器
const hasSuccessfulCompiles = results.some(r => r.success);
if (hasSuccessfulCompiles) {
await eventEmitter.emit("server-restart-needed", { results });
}
// 如果在处理过程中又有新任务加入,继续处理
if (queue.length > 0) {
processQueue();
}
};
return {
addTask,
getQueueLength: () => queue.length,
isProcessing: () => isProcessing,
getProcessingCount: () => processingCount,
setTaskProcessor: (processor) => {
taskProcessor = processor;
}
};
};
const defaultConfig = {
autoUpdateI18n: true,
polyfill: {
console: {
enable: true,
@ -44,22 +144,22 @@ const defaultConfig = {
},
},
lifecycle: {
onInit: () => {
onInit: (config) => {
console.log("----> Watcher initialized.");
},
onServerStart: () => {
onServerStart: (config) => {
console.log("----> Server started.");
},
onBeforeCompile: () => {
onBeforeCompile: (config) => {
console.log("----> Compiling......");
},
onAfterCompile: () => {
onAfterCompile: (config) => {
console.log("----> Compile completed.");
},
onServerShutdown: () => {
onServerShutdown: (config) => {
console.log("----> Server shutdown.");
},
onDispose: () => {
onDispose: (config) => {
console.log("----> Watcher disposed.");
},
},
@ -94,62 +194,71 @@ const getOverrideConfig = (config) => {
? overrideInner(config, defaultConfig)
: defaultConfig;
};
/**
* 根据 alias 配置表将路径中的别名替换为真实路径
* @param path - 输入的路径字符串例如 "@project/file" "@oak-app-domain/some-module"
* @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组
* @returns 替换后的路径如果没有匹配到 alias则返回原始路径
*/
function replaceAliasWithPath(path, aliasConfig) {
for (const [alias, targets] of Object.entries(aliasConfig)) {
// If alias ends with "*", handle it as a dynamic alias.
if (alias.endsWith('*')) {
// 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 match = path.match(aliasPattern);
if (match) {
// 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)
// Ensure that the target path ends with a slash if it's not already
const replacedPath = target.replace(/\/\*$/, "/") + match[1];
return replacedPath;
}
// 当前运行目录
const pwd = process.cwd();
// 创建服务器管理器
const createServerManager = (projectPath, eventEmitter, config) => {
let shutdown;
let isRestarting = false;
const restart = async () => {
if (isRestarting) {
return;
}
else {
// Handle static alias without "*" by directly matching the path
if (path.startsWith(alias)) {
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
return path.replace(alias, target);
}
isRestarting = true;
if (shutdown) {
console.log("----> Shutting down service......");
await shutdown().then(() => config.lifecycle.onServerShutdown(config));
shutdown = undefined;
}
}
// If no alias matches, return the original path
return path;
}
const watch = (projectPath, config) => {
const realConfig = getOverrideConfig(config);
const enableTrace = !!process.env.ENABLE_TRACE;
// 查找配置文件
const configFileName = typescript_1.default.findConfigFile(projectPath, typescript_1.default.sys.fileExists, "tsconfig.build.json");
if (!configFileName) {
throw new Error("Could not find a valid 'tsconfig.build.json'.");
}
// 读取配置文件
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 = (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"));
});
// 输出真实的alias配置
console.debug("[DEBUG] Running Alias config:", aliasConfig);
const createProgramAndSourceFile = (path, options, projectReferences) => {
console.warn("----> Clearing require cache of project......");
let deleteCount = 0;
// 清空lib以下目录的缓存
Object.keys(require.cache).forEach((key) => {
// 如果不是项目目录下的文件,不删除
if (!key.startsWith(projectPath)) {
return;
}
else if (key.includes("lib") &&
!key.includes("node_modules")) {
delete require.cache[key];
deleteCount++;
}
});
console.warn(`----> ${deleteCount} modules has been removed from require.cache.`);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
console.warn("----> Starting service......");
try {
(0, timer_manager_1.clearAllTimers)();
// 这里注意要在require之前因为require会触发编译
const { startup } = require('./start');
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;
await eventEmitter.emit("server-restarted", {});
};
const dispose = async () => {
if (shutdown) {
await shutdown();
}
};
return {
restart,
dispose,
isRestarting: () => isRestarting
};
};
// 创建编译器
const createCompiler = async (projectPath, options, projectReferences, treatFile, config) => {
const createProgramAndSourceFile = (path) => {
const program = typescript_1.default.createProgram({
rootNames: [path],
options,
@ -171,8 +280,248 @@ const watch = (projectPath, config) => {
}
return { program, sourceFile, diagnostics };
};
// 这个函数用于解决require的时候如果文件不存在会尝试编译ts文件
// 测试在src下新建一个ts文件然后删除lib下的js文件然后require这个文件会发现会自动编译ts文件
const compileTask = async (task) => {
const { filePath, changeType } = task;
const modulePath = path_1.default.resolve(filePath);
// 判断文件类型
if (!filePath.endsWith(".ts")) {
// 处理非TypeScript文件 (如JSON文件)
if (filePath.endsWith(".json")) {
const targetPath = modulePath.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"));
try {
if (changeType === "remove") {
if (fs_1.default.existsSync(targetPath)) {
fs_1.default.unlinkSync(targetPath);
}
console.warn(`File ${targetPath} has been removed.`);
}
else if (changeType === "add" || changeType === "change") {
// 确保目录存在
const dir = path_1.default.dirname(targetPath);
if (!fs_1.default.existsSync(dir)) {
fs_1.default.mkdirSync(dir, { recursive: true });
}
if (changeType === "change" && fs_1.default.existsSync(targetPath)) {
fs_1.default.unlinkSync(targetPath);
}
fs_1.default.copyFileSync(filePath, targetPath, fs_1.default.constants.COPYFILE_FICLONE);
console.warn(`File ${filePath} has been copied to ${targetPath}.`);
}
return {
taskId: task.id,
success: true,
filePath
};
}
catch (error) {
return {
taskId: task.id,
success: false,
filePath,
error: error instanceof Error ? error.message : String(error)
};
}
}
else {
console.warn(`File ${filePath} is not [ts,json] file, skipped.`);
return {
taskId: task.id,
success: true,
filePath
};
}
}
// 处理TypeScript文件
console.clear();
console.warn(`File ${filePath} has been ${changeType}d`);
const libPath = modulePath
.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"))
.replace(/\.ts$/, ".js");
if (changeType === "remove") {
try {
if (fs_1.default.existsSync(libPath)) {
fs_1.default.unlinkSync(libPath);
}
console.warn(`File ${libPath} has been removed.`);
return {
taskId: task.id,
success: true,
filePath
};
}
catch (error) {
return {
taskId: task.id,
success: false,
filePath,
error: `Error removing file: ${error instanceof Error ? error.message : String(error)}`
};
}
}
// 编译TypeScript文件
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(filePath);
if (diagnostics.length) {
return {
taskId: task.id,
success: false,
filePath,
error: "TypeScript compilation error"
};
}
// 只输出单个文件
config.lifecycle.onBeforeCompile(config);
const emitResult = program.emit(sourceFile);
if (emitResult.emitSkipped) {
console.error(`Emit failed for ${filePath}!`);
config.lifecycle.onAfterCompile(config);
return {
taskId: task.id,
success: false,
filePath,
error: "TypeScript emit failed"
};
}
else {
console.log(`Emit succeeded for ${filePath}.`);
config.lifecycle.onAfterCompile(config);
const jsFilePath = libPath;
treatFile(jsFilePath);
return {
taskId: task.id,
success: true,
filePath
};
}
};
return {
compileTask
};
};
// 创建文件监视器
const createFileWatcher = (projectPath, eventEmitter) => {
const watchSourcePath = path_1.default.join(projectPath, "src");
let startWatching = false;
console.log("Watching for changes in", watchSourcePath);
const watcher = chokidar_1.default.watch(watchSourcePath, {
persistent: true,
ignored: (file) => file.endsWith(".tsx") ||
file.endsWith(".xml") ||
file.includes("components") ||
file.includes("pages") ||
file.includes("hooks"),
interval: 100,
binaryInterval: 100,
cwd: projectPath,
depth: 99,
followSymlinks: true,
ignoreInitial: false,
ignorePermissionErrors: false,
usePolling: false,
alwaysStat: false,
});
const handleFileChange = async (path, type) => {
if (!startWatching) {
return;
}
const event = {
path,
type,
timestamp: Date.now()
};
await eventEmitter.emit("file-changed", event);
};
watcher.on("ready", () => {
console.warn("Initial scan complete. Ready for changes");
startWatching = true;
});
watcher.on("error", (error) => console.log(`Watcher error: ${error}`));
watcher
.on("add", async (path) => {
await handleFileChange(path, "add");
})
.on("change", async (path) => {
await handleFileChange(path, "change");
})
.on("unlink", async (path) => {
await handleFileChange(path, "remove");
});
const dispose = async () => {
await watcher.close();
};
return {
dispose
};
};
// 生成唯一任务ID
const generateTaskId = () => {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
// /**
// * 根据 alias 配置表将路径中的别名替换为真实路径
// * @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
// * @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组
// * @returns 替换后的路径,如果没有匹配到 alias则返回原始路径
// */
// const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => {
// for (const [alias, targets] of Object.entries(aliasConfig)) {
// // If alias ends with "*", handle it as a dynamic alias.
// if (alias.endsWith('*')) {
// // 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 match = path.match(aliasPattern);
// if (match) {
// // 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)
// // Ensure that the target path ends with a slash if it's not already
// const replacedPath = target.replace(/\/\*$/, "/") + match[1];
// return replacedPath;
// }
// } else {
// // Handle static alias without "*" by directly matching the path
// if (path.startsWith(alias)) {
// 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
// return path.replace(alias, target);
// }
// }
// }
// // If no alias matches, return the original path
// return path;
// };
const watch = async (projectPath, config) => {
const realConfig = getOverrideConfig(config);
const enableTrace = !!process.env.ENABLE_TRACE;
// 查找配置文件
const configFileName = typescript_1.default.findConfigFile(projectPath, typescript_1.default.sys.fileExists, "tsconfig.build.json");
if (!configFileName) {
throw new Error("Could not find a valid 'tsconfig.build.json'.");
}
const runFile = await (0, tsc_alias_1.prepareSingleFileReplaceTscAliasPaths)({
configFile: path_1.default.join(projectPath, "tsconfig.build.json"),
resolveFullPaths: true,
});
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配置
// console.debug("[DEBUG] Running Alias config:", aliasConfig);
// 初始化polyfill
const polyfillLoader = () => {
const BuiltinModule = require("module");
// 模拟环境下的 module 构造函数
@ -186,7 +535,13 @@ const watch = (projectPath, config) => {
// 检查并编译 .ts 文件
if (fs_1.default.existsSync(tsPath)) {
console.log(`[resolve] Found TypeScript source file: ${tsPath}, attempting to compile...`);
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(tsPath, options, projectReferences);
const program = typescript_1.default.createProgram({
rootNames: [tsPath],
options,
projectReferences,
});
const sourceFile = program.getSourceFile(tsPath);
const diagnostics = typescript_1.default.getPreEmitDiagnostics(program, sourceFile);
if (diagnostics.length) {
console.error(`[resolve] Compilation failed for: ${tsPath}`);
throw new Error("TypeScript compilation error");
@ -196,6 +551,9 @@ const watch = (projectPath, config) => {
console.error(`[resolve] Emit skipped for: ${tsPath}`);
throw new Error("TypeScript emit skipped");
}
else {
treatFile(jsPath);
}
console.log(`[resolve] Successfully compiled: ${tsPath}`);
return jsPath;
}
@ -225,11 +583,11 @@ const watch = (projectPath, config) => {
rFoptions // 解析选项
) {
let resolvedRequest = request;
const replacedPath = replaceAliasWithPath(request, aliasConfig);
if (replacedPath !== request) {
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
resolvedRequest = path_1.default.join(projectPath, replacedPath);
}
// const replacedPath = replaceAliasWithPath(request, aliasConfig);
// if (replacedPath !== request) {
// console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
// resolvedRequest = pathLib.join(projectPath, replacedPath);
// }
try {
return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
}
@ -247,290 +605,144 @@ const watch = (projectPath, config) => {
}
};
};
// 初始化polyfill
polyfillLoader();
//polyfill console.log 添加时间
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(...(defaultConfig.polyfill?.console?.formatter({
level: levelStr,
caller: getCallerInfo(),
args,
}) || []));
};
});
};
realConfig.polyfill.console.enable && polyfillConsole(enableTrace);
// 这里注意要在require之前因为require会触发编译
const { startup } = require('./start');
// 如果lib目录是空的则直接编译所有的ts文件
const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js");
if (!fs_1.default.existsSync(serverConfigFile)) {
// 尝试编译src/configuration/server.ts
console.log(`[watch] Server configuration file not found, attempting to compile the project......`);
const tryCompile = (tsFile) => {
console.log(`[watch] Compiling: ${tsFile}`);
if (fs_1.default.existsSync(tsFile)) {
const { program, diagnostics } = createProgramAndSourceFile(tsFile, options, projectReferences);
if (diagnostics.length) {
console.error(`Error in ${tsFile}`);
diagnostics.forEach((diagnostic) => {
console.error(`${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`);
realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter);
// 监听定时器相关的API,确保定时器在重启后不会重复触发
(0, timer_manager_1.hookTimers)();
// 初始编译检查
const initialCompile = () => {
const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js");
if (!fs_1.default.existsSync(serverConfigFile)) {
// 尝试编译src/configuration/server.ts
console.log(`[watch] Server configuration file not found, attempting to compile the project......`);
const tryCompile = (tsFile) => {
console.log(`[watch] Compiling: ${tsFile}`);
if (fs_1.default.existsSync(tsFile)) {
const program = typescript_1.default.createProgram({
rootNames: [tsFile],
options,
projectReferences,
});
console.error(`文件存在语法错误,请检查修复后重试!`);
process.exit(1);
const diagnostics = typescript_1.default.getPreEmitDiagnostics(program);
if (diagnostics.length) {
console.error(`Error in ${tsFile}`);
diagnostics.forEach((diagnostic) => {
console.error(`${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`);
});
console.error(`文件存在语法错误,请检查修复后重试!`);
process.exit(1);
}
// 编译所有的文件
const emitResult = program.emit();
if (emitResult.emitSkipped) {
console.error(`Emit failed for ${tsFile}!`);
process.exit(1);
}
console.log(`Emit succeeded. ${tsFile} has been compiled.`);
}
// const emitResult = program.emit(sourceFile);
// 编译所有的文件
const emitResult = program.emit();
if (emitResult.emitSkipped) {
console.error(`Emit failed for ${tsFile}!`);
process.exit(1);
}
console.log(`Emit succeeded. ${tsFile} has been compiled.`);
}
};
// 所有要编译的目录
// 其他涉及到的目录会在运行的时候自动编译这里主要处理的是动态require的文件
const compileFiles = [
"src/configuration/index.ts",
"src/aspects/index.ts",
"src/checkers/index.ts",
"src/triggers/index.ts",
"src/timers/index.ts",
"src/routines/start.ts",
"src/watchers/index.ts",
"src/endpoints/index.ts",
"src/data/index.ts",
"src/ports/index.ts",
];
compileFiles.forEach(tryCompile);
}
return new Promise((resolve, reject) => {
realConfig.lifecycle.onInit();
let shutdown;
const restart = async () => {
if (shutdown) {
console.log("----> Shutting down service......");
await shutdown().then(realConfig.lifecycle.onServerShutdown);
// reset shutdown
shutdown = undefined;
}
console.warn("----> Clearing require cache of project......");
let deleteCount = 0;
// 清空lib以下目录的缓存
Object.keys(require.cache).forEach((key) => {
// 如果不是项目目录下的文件,不删除
if (!key.startsWith(projectPath)) {
return;
}
else if (key.includes("lib") &&
!key.includes("node_modules")) {
delete require.cache[key];
deleteCount++;
};
// 所有要编译的目录
// 其他涉及到的目录会在运行的时候自动编译这里主要处理的是动态require的文件
const compileFiles = [
"src/configuration/index.ts",
"src/aspects/index.ts",
"src/checkers/index.ts",
"src/triggers/index.ts",
"src/timers/index.ts",
"src/routines/start.ts",
"src/watchers/index.ts",
"src/endpoints/index.ts",
"src/data/index.ts",
"src/ports/index.ts",
];
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(async (resolve, reject) => {
// 创建事件系统
const eventEmitter = createEventEmitter();
// 创建各个组件
const compileQueue = createCompileQueue(eventEmitter);
const compiler = await createCompiler(projectPath, options, projectReferences, treatFile, realConfig);
const serverManager = createServerManager(projectPath, eventEmitter, realConfig);
const fileWatcher = createFileWatcher(projectPath, eventEmitter);
// 设置编译器处理器
compileQueue.setTaskProcessor(compiler.compileTask);
// 设置事件监听器
eventEmitter.on("file-changed", (event) => {
const task = {
id: generateTaskId(),
filePath: event.path,
changeType: event.type,
timestamp: event.timestamp
};
compileQueue.addTask(task);
});
eventEmitter.on("compile-batch-started", (data) => {
console.log(`----> Starting compilation batch (${data.count} files)...`);
});
eventEmitter.on("compile-batch-completed", (data) => {
const successCount = data.results.filter(r => r.success).length;
console.log(`----> Compilation batch completed: ${successCount}/${data.results.length} successful`);
});
// 编译成功之后若设置的同步i18n则触发i18n更新
if (realConfig.autoUpdateI18n) {
const projectI18nPath = path_1.default.join("src", "data", "i18n.ts");
eventEmitter.on("compile-task-completed", async (result) => {
if (result.filePath == projectI18nPath && result.success) {
console.log("-------------start upgrade i18n.-------------");
// 这里是copyupgradeI18n的
const { checkAndUpdateI18n } = require('oak-backend-base/lib/routines/i18n.js');
const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
// 这里注意要在require之前因为require会触发编译
const { startup } = require('./start');
startup(pwd, simpleConnector, true, true, checkAndUpdateI18n).then(() => {
console.log("------------upgrade i18n success.------------");
}).catch((err) => {
console.error("------------upgrade i18n failed!------------", err);
});
}
});
console.warn(`----> ${deleteCount} modules has been removed from require.cache.`);
const pwd = process.cwd();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
console.warn("----> Starting service......");
shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
realConfig.lifecycle.onServerStart();
return shutdown;
});
};
const watchSourcePath = path_1.default.join(projectPath, "src");
console.log("Watching for changes in", watchSourcePath);
const watcher = chokidar_1.default.watch(watchSourcePath, {
persistent: true,
ignored: (file) => file.endsWith(".tsx") ||
file.endsWith(".xml") ||
file.includes("components") ||
file.includes("pages") ||
file.includes("hooks"),
interval: 100,
binaryInterval: 100,
cwd: projectPath,
depth: 99,
followSymlinks: true,
ignoreInitial: false,
ignorePermissionErrors: false,
usePolling: false,
alwaysStat: false,
});
let startWatching = false;
watcher.on("ready", () => {
console.warn("Initial scan complete. Ready for changes");
startWatching = true;
});
watcher.on("error", (error) => console.log(`Watcher error: ${error}`));
let processingQueue = [];
const fileChangeHandler = async (path, type) => {
// 判断一下是不是以下扩展名ts
if (!path.endsWith(".ts")) {
// 如果是json文件复制或者删除
if (path.endsWith(".json")) {
// const targetPath = path.replace("src", "lib"); // 这里直接替换不对,应该是拿到项目目录,把项目目录/src 换成项目目录/lib
const targetPath = path.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"));
if (type === "remove") {
fs_1.default.unlinkSync(targetPath);
console.warn(`File ${targetPath} has been removed.`);
}
else if (type === "add") {
fs_1.default.copyFileSync(path, targetPath, fs_1.default.constants.COPYFILE_FICLONE);
console.warn(`File ${path} has been created at ${targetPath}.`);
}
else if (type === "change") {
// 强制覆盖文件
if (fs_1.default.existsSync(targetPath)) {
fs_1.default.unlinkSync(targetPath);
}
fs_1.default.copyFileSync(path, targetPath, fs_1.default.constants.COPYFILE_FICLONE);
console.warn(`File ${path} has been copied to ${targetPath}.`);
}
}
else {
console.warn(`File ${path} is not [ts,json] file, skiped.`);
}
return;
}
// 控制台清空
console.clear();
console.warn(`File ${path} has been ${type}d`);
// 先判断一下这个文件在不在require.cache里面
const modulePath = path_1.default.resolve(path);
// 将src替换为lib
const libPath = modulePath
.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"))
.replace(/\.ts$/, ".js");
let compileOnly = false;
if (!require.cache[libPath]) {
// 如果是删除的话直接尝试删除lib下的文件
if (type === "remove") {
try {
fs_1.default.unlinkSync(libPath);
}
catch (e) {
console.error(`Error in delete ${libPath}`, e);
}
console.warn(`File ${libPath} has been removed.`);
return;
}
// console.warn(
// `File ${libPath} is not in module cache, will compile only.`
// );
// compileOnly = true;
// 这里现在不需要仅编译了因为在require的时候会自动编译ts文件
}
else {
// 如果是删除,则需要发出警告,文件正在被进程使用
if (type === "remove") {
console.error(`File ${libPath} is being used, skiped.`);
return;
}
}
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(path, options, projectReferences);
if (diagnostics.length) {
return;
}
// 只输出单个文件
realConfig.lifecycle.onBeforeCompile();
const emitResult = program.emit(sourceFile);
// 是否成功
const result = emitResult.emitSkipped;
if (result) {
console.error(`Emit failed for ${path}!`);
realConfig.lifecycle.onAfterCompile();
}
else {
console.log(`Emit succeeded. ${compileOnly ? "" : "reload service......"}`);
realConfig.lifecycle.onAfterCompile();
if (compileOnly) {
return;
}
// await restart(); // 只有在队列里面的最后一个文件编译完成后才会重启服务
if (processingQueue.length === 1) {
await restart();
}
else {
console.log("Waiting for other operations to complete...");
}
}
};
const onChangeDebounced = async (path, type) => {
if (processingQueue.includes(path)) {
console.log("Processing, please wait...");
return;
}
try {
processingQueue.push(path);
await fileChangeHandler(path, type);
}
catch (e) {
console.clear();
console.error(e);
process.exit(1);
}
finally {
processingQueue = processingQueue.filter((p) => p !== path);
}
};
watcher
.on("add", async (path) => {
if (startWatching) {
await onChangeDebounced(path, "add");
}
})
.on("change", async (path) => {
if (startWatching) {
await onChangeDebounced(path, "change");
}
})
.on("unlink", async (path) => {
if (startWatching) {
await onChangeDebounced(path, "remove");
}
eventEmitter.on("server-restart-needed", async () => {
if (!serverManager.isRestarting()) {
console.log("----> Restarting server...");
await serverManager.restart();
}
});
const dispose = async () => {
if (shutdown) {
await shutdown();
}
await watcher.close();
realConfig.lifecycle.onDispose();
};
restart()
// 初始化
realConfig.lifecycle.onInit(realConfig);
// 执行初始编译
initialCompile();
// 启动服务器
serverManager.restart()
.then(() => {
const dispose = async () => {
await fileWatcher.dispose();
await serverManager.dispose();
realConfig.lifecycle.onDispose(realConfig);
};
resolve(dispose);
})
.catch(reject);

View File

@ -1,6 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateCompilerJsContent = exports.oakConfigContentWithWeb = exports.oakConfigContentWithWeChatMp = exports.appJsonContentWithWeChatMp = exports.projectConfigContentWithWeChatMp = exports.tsConfigWebJsonContent = exports.tsConfigMpJsonContent = exports.tsConfigPathsJsonContent = exports.tsConfigBuildJsonContent = exports.tsConfigJsonContent = exports.packageJsonContent = void 0;
exports.packageJsonContent = packageJsonContent;
exports.tsConfigJsonContent = tsConfigJsonContent;
exports.tsConfigBuildJsonContent = tsConfigBuildJsonContent;
exports.tsConfigPathsJsonContent = tsConfigPathsJsonContent;
exports.tsConfigMpJsonContent = tsConfigMpJsonContent;
exports.tsConfigWebJsonContent = tsConfigWebJsonContent;
exports.projectConfigContentWithWeChatMp = projectConfigContentWithWeChatMp;
exports.appJsonContentWithWeChatMp = appJsonContentWithWeChatMp;
exports.oakConfigContentWithWeChatMp = oakConfigContentWithWeChatMp;
exports.oakConfigContentWithWeb = oakConfigContentWithWeb;
exports.updateCompilerJsContent = updateCompilerJsContent;
const tslib_1 = require("tslib");
const child_process_1 = require("child_process");
const fs_1 = require("fs");
@ -75,7 +85,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
"build:mp": "${cliBinName} build --target mp --mode production",
"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:server": "${cliBinName} start --target web --mode development",
"start:native": "${cliBinName} start --target rn --mode development --devMode frontend",
@ -87,7 +97,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
"build-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap",
"build-analyze:web": "${cliBinName} build --target web --mode production --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",
"run:ios": "oak-cli run -p ios",
"run:android": "oak-cli run -p android",
@ -300,7 +310,6 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
}
`;
}
exports.packageJsonContent = packageJsonContent;
function tsConfigJsonContent() {
return `{
"extends": "./tsconfig.paths.json",
@ -352,7 +361,6 @@ function tsConfigJsonContent() {
]
}`;
}
exports.tsConfigJsonContent = tsConfigJsonContent;
function tsConfigBuildJsonContent() {
return `{
"extends": "./tsconfig.build.paths.json",
@ -389,10 +397,15 @@ function tsConfigBuildJsonContent() {
"test",
"src/pages/**/*",
"src/components/**/*"
]
],
"oakBuildChecks": {
"context": {
"checkAsyncContext": true,
"targetModules": ["context/BackendRuntimeContext"]
}
}
}`;
}
exports.tsConfigBuildJsonContent = tsConfigBuildJsonContent;
function tsConfigPathsJsonContent(deps) {
const paths = {
"@project/*": [
@ -417,11 +430,10 @@ function tsConfigPathsJsonContent(deps) {
compilerOptions: {
baseUrl: "./",
paths,
typeRoots: ["./typings"]
typeRoots: ["./typings", "node_modules/@types"]
}
}, null, '\t');
}
exports.tsConfigPathsJsonContent = tsConfigPathsJsonContent;
function tsConfigMpJsonContent() {
return `{
"extends": "./tsconfig.paths.json",
@ -469,7 +481,6 @@ function tsConfigMpJsonContent() {
]
}`;
}
exports.tsConfigMpJsonContent = tsConfigMpJsonContent;
function tsConfigWebJsonContent() {
return `{
"extends": "./tsconfig.paths.json",
@ -519,7 +530,6 @@ function tsConfigWebJsonContent() {
]
}`;
}
exports.tsConfigWebJsonContent = tsConfigWebJsonContent;
function projectConfigContentWithWeChatMp(oakConfigName, projectname, miniVersion) {
return `{
"description": "项目配置文件",
@ -595,7 +605,6 @@ function projectConfigContentWithWeChatMp(oakConfigName, projectname, miniVersio
}
}`;
}
exports.projectConfigContentWithWeChatMp = projectConfigContentWithWeChatMp;
function appJsonContentWithWeChatMp(isDev) {
const pages = [
'@project/pages/store/list/index',
@ -619,25 +628,27 @@ function appJsonContentWithWeChatMp(isDev) {
"sitemapLocation": "sitemap.json"
}`;
}
exports.appJsonContentWithWeChatMp = appJsonContentWithWeChatMp;
function oakConfigContentWithWeChatMp() {
return `{
"theme": {
}
}`;
}
exports.oakConfigContentWithWeChatMp = oakConfigContentWithWeChatMp;
function oakConfigContentWithWeb() {
return `{
"theme": {
}
}`;
}
exports.oakConfigContentWithWeb = oakConfigContentWithWeb;
function updateCompilerJsContent(directory, deps) {
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 (!(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 { program } = ast;
const { body } = program;
@ -692,4 +703,3 @@ function updateCompilerJsContent(directory, deps) {
(0, fs_1.writeFileSync)(compilerJsPath, code);
}
}
exports.updateCompilerJsContent = updateCompilerJsContent;

6
lib/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import { BaseEntityDict } from "oak-domain";
import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore";
import { EntityDict } from "oak-domain/lib/types";
export type InternalErrorType = 'aspect' | 'trigger' | 'watcher' | 'timer' | 'checkpoint';
export type InternalErrorHandler<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> = (ctx: Cxt, type: InternalErrorType, message: string, err: Error) => Promise<void>;
export type ExceptionPublisher = (type: string, message: string, err: any) => Promise<void>;

2
lib/types/index.js Normal file
View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@ -1,6 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkNodeVersion = exports.randomString = exports.deWeight = exports.formatJsonByFile = exports.union = exports.intersect = exports.difference = exports.getStr = exports.findJson = void 0;
exports.findJson = findJson;
exports.getStr = getStr;
exports.difference = difference;
exports.intersect = intersect;
exports.union = union;
exports.formatJsonByFile = formatJsonByFile;
exports.deWeight = deWeight;
exports.randomString = randomString;
exports.checkNodeVersion = checkNodeVersion;
const tslib_1 = require("tslib");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
/**
@ -20,7 +28,6 @@ function findJson(pathArr) {
}
return result;
}
exports.findJson = findJson;
/**
* @name 已知前后文取中间文本
* @export
@ -34,7 +41,6 @@ function getStr(str, start, end) {
let res = str.match(reg);
return res ? res[1] : null;
}
exports.getStr = getStr;
/**
* @name 差集
* @export
@ -46,7 +52,6 @@ exports.getStr = getStr;
function difference(current, target) {
return new Set([...target].filter(x => !current.has(x)));
}
exports.difference = difference;
/**
* @name 获取交集
* @export
@ -58,7 +63,6 @@ exports.difference = difference;
function intersect(current, target) {
return new Set([...target].filter(x => current.has(x)));
}
exports.intersect = intersect;
/**
* @name 获取并集
* @export
@ -70,7 +74,6 @@ exports.intersect = intersect;
function union(current, target) {
return new Set([...current, ...target]);
}
exports.union = union;
/**
* @name 格式化json
* @export
@ -81,7 +84,6 @@ exports.union = union;
function formatJsonByFile(data) {
return JSON.stringify(data, null, 2);
}
exports.formatJsonByFile = formatJsonByFile;
/**
* @name 数组对象去重
* @export
@ -98,7 +100,6 @@ function deWeight(arr, type) {
}
return new Set([...map.values()]);
}
exports.deWeight = deWeight;
/**
* @name 随机字符串
* @export
@ -114,7 +115,6 @@ function randomString(length) {
}
return result;
}
exports.randomString = randomString;
/**
* @name 检查当前nodejs运行时版本
* @export
@ -135,4 +135,3 @@ function checkNodeVersion() {
process.exit(-1);
}
}
exports.checkNodeVersion = checkNodeVersion;

View File

@ -1,6 +1,6 @@
{
"name": "@xuchangzju/oak-cli",
"version": "4.0.25",
"version": "4.0.33",
"description": "client for oak framework",
"main": "lib/index.js",
"scripts": {
@ -44,7 +44,7 @@
"babel-plugin-module-resolver": "^5.0.0",
"events": "^3.3.0",
"fork-ts-checker-webpack-plugin": "^8.0.0",
"ts-node": "^10.9.1",
"ts-node": "^10.9.2",
"typescript": "^5.2.2"
},
"dependencies": {
@ -112,9 +112,9 @@
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.5.3",
"node-watch": "^0.7.4",
"oak-backend-base": "^4.1.21",
"oak-domain": "^5.1.27",
"oak-frontend-base": "^5.3.34",
"oak-backend-base": "file:../oak-backend-base",
"oak-domain": "file:../oak-domain",
"oak-frontend-base": "file:../oak-frontend-base",
"parse-asn1": "5.1.6",
"postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2",
@ -145,6 +145,7 @@
"stylelint-webpack-plugin": "^3.2.0",
"tailwindcss": "^3.0.2",
"terser-webpack-plugin": "^5.2.5",
"tsc-alias": "^1.8.16",
"tslib": "^2.4.0",
"ui-extract-webpack-plugin": "^1.0.0",
"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'));
buildSchema(join(process.cwd(), 'src', 'oak-app-domain'));

View File

@ -1,5 +1,5 @@
import * as ts from 'typescript';
import { writeFileSync } from 'fs';
import { writeFileSync, readFileSync, readdirSync, existsSync, mkdirSync } from 'fs';
const { factory } = ts;
import {
@ -279,8 +279,22 @@ export async function create(dirName: string, cmd: any) {
checkFileExistsAndCreate(rootPath);
// 复制项目文件
if (isModule) {
// 模块化的项目只拷贝src和typings目录
copyFolder(join(emptyTemplatePath, 'src'), join(rootPath, 'src'));
// 模块化的项目,只拷贝 src 下的内容,但跳过 pages 目录;同时拷贝 typings
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'));
}
else {
@ -356,8 +370,24 @@ export async function create(dirName: string, cmd: any) {
tsConfigWebJson,
checkFileExistsAndCreateType.FILE
);
// 更新configuration/compiler.js
updateCompilerJsContent(rootPath, deps);
// 复制.gitignore
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(
`Successfully created project ${primary(
@ -390,9 +420,25 @@ export async function create(dirName: string, cmd: any) {
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
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 { checkFileExistsAndCreateType } from './enum'
import { Error, error, Warn, warn } from './tip-style'
@ -141,9 +141,8 @@ export function copyFolder(currentDir: PathLike, targetDir: PathLike, overwrite:
}
else {
if (file.isFile()) {
const readStream = createReadStream(copyCurrentFileInfo);
const writeStream = createWriteStream(copyTargetFileInfo);
readStream.pipe(writeStream);
// 使用同步复制确保文件完全写入
copyFileSync(copyCurrentFileInfo, copyTargetFileInfo);
// console.log(`复制文件: ${copyCurrentFileInfo} -> ${copyTargetFileInfo}`);
} else {
try {

View File

@ -7,10 +7,10 @@ import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRunt
export async function initialize<
ED extends EntityDict & BaseEntityDict,
Cxt extends BackendRuntimeContext<ED>
>(path: string) {
>(path: string, ifExists?: 'drop' | 'omit' | 'dropIfNotStatic') {
const appLoader = new AppLoader(path);
await appLoader.mount(true);
await appLoader.initialize();
await appLoader.initialize(ifExists);
await appLoader.unmount();
console.log('data initialized');
}

View File

@ -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);
});
}
};

View File

@ -5,11 +5,11 @@ import PathLib, { join } from 'path';
import Koa from 'koa';
import KoaRouter from 'koa-router';
import KoaBody from 'koa-body';
import logger from 'koa-logger';
// import logger from 'koa-logger';
import { AppLoader, getClusterInfo, ClusterAppLoader } from 'oak-backend-base';
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 { AsyncRowStore, AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
@ -54,6 +54,20 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
omitTimers?: boolean,
routine?: (context: AsyncContext<ED>) => Promise<void>,
): Promise<(() => Promise<any>) | any> {
// let errorHandler: InternalErrorHandler<ED, Cxt> | undefined = undefined;
// try {
// errorHandler = require(join(
// path,
// 'lib',
// 'configuration',
// 'exception'
// )).default;
// } catch (err) {
// // 不存在exception配置
// }
const serverConfiguration: ServerConfiguration = require(join(
path,
'lib',
@ -73,7 +87,8 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
const koa = new Koa();
// 使用 koa-logger 中间件打印日志
koa.use(logger());
// koa.use(logger());
// socket
const httpServer = createServer(koa.callback());
const socketPath = connector.getSocketPath();
@ -200,10 +215,27 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
if (routine) {
// 如果传入了routine执行完成后就结束
const result = await appLoader.execRoutine(routine);
await appLoader.unmount();
// await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
return result;
}
// if (errorHandler && typeof errorHandler === 'function') {
// // polyfillConsole("startup", true, (props) => {
// // if (props.level === "error") {
// // appLoader.execRoutine(async (ctx) => {
// // await errorHandler(props.caller, props.args, ctx);
// // }).catch((err) => {
// // console.warn('执行全局错误处理失败:', err);
// // });
// // }
// // return props.args;
// // });
// // appLoader.registerInternalErrorHandler(async (ctx, type, msg, err) => {
// // await errorHandler(ctx, type, msg, err);
// // });
// }
appLoader.regAllExceptionHandler()
// 否则启动服务器模式
koa.use(async (ctx, next) => {
try {
@ -212,26 +244,49 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
console.error(err);
const { request } = ctx;
const exception =
err instanceof OakException
isOakException(err)
? err
: new OakException<ED>(
serverConfiguration?.internalExceptionMask ||
ExceptionMask
);
const { body } = connector.serializeException(
const { body, headers } = connector.serializeException(
exception,
request.headers,
request.body
);
ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key])
})
return;
}
});
koa.use(
KoaBody(Object.assign({
multipart: true,
}, 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();
// 如果是开发环境允许options
@ -240,6 +295,10 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
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') {
ctx.body = 200;
} else {
@ -278,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) => {
const { request } = ctx;
const { contextString, aspectName, data } = connector.parseRequest(
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: any;
let opRecords: OpRecord<ED>[] = [];
let message: string | undefined = 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,
@ -300,6 +387,12 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
message
);
ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key])
})
return;
});
@ -442,16 +535,70 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
appLoader.startTimers();
}
process.on('SIGINT', async () => {
await appLoader.unmount();
process.exit(0);
let isShutingdown = false;
const shutdown = async () => {
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 () => {
await httpServer.close();
await koa.removeAllListeners();
await appLoader.unmount();
}
// SIGTERM - 系统/容器管理器发送的终止信号Docker、K8s、PM2等
process.on('SIGTERM', () => {
handleShutdown('SIGTERM');
});
// SIGQUIT - Ctrl+\ 发送的退出信号
process.on('SIGQUIT', () => {
handleShutdown('SIGQUIT');
});
// SIGHUP - 终端关闭时发送(可选)
process.on('SIGHUP', () => {
handleShutdown('SIGHUP');
});
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();

File diff suppressed because it is too large Load Diff

View File

@ -92,7 +92,7 @@ export function packageJsonContent({
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
"build:mp": "${cliBinName} build --target mp --mode production",
"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:server": "${cliBinName} start --target web --mode development",
"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-analyze:web": "${cliBinName} build --target web --mode production --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",
"run:ios": "oak-cli run -p ios",
"run:android": "oak-cli run -p android",
@ -406,7 +406,13 @@ export function tsConfigBuildJsonContent() {
"test",
"src/pages/**/*",
"src/components/**/*"
]
],
"oakBuildChecks": {
"context": {
"checkAsyncContext": true,
"targetModules": ["context/BackendRuntimeContext"]
}
}
}`;
}
@ -436,7 +442,7 @@ export function tsConfigPathsJsonContent(deps: string[]) {
compilerOptions: {
baseUrl: "./",
paths,
typeRoots: ["./typings"]
typeRoots: ["./typings", "node_modules/@types"]
}
}, null, '\t');
}
@ -663,9 +669,14 @@ export function oakConfigContentWithWeb() {
export function updateCompilerJsContent(directory: string, deps: string[]) {
const compilerJsPath = join(directory, 'configuration', 'compiler.js');
assert(existsSync(compilerJsPath));
// 只有在有依赖项时才需要修改 compiler.js
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 { program } = ast!;
const { body } = program!;

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

@ -1,11 +1,12 @@
const { initialize } = require('@xuchangzju/oak-cli/lib/server/initialize');
const assert = require('assert');
const pwd = process.cwd();
const dropIfExists = process.argv[2];
// console.log(dropIfExists);
const ifExists = process.argv[2];
assert(!ifExists || ['drop' , 'omit' , 'dropIfNotStatic'].includes(ifExists), "第二个参数只能是'drop' | 'omit' | 'dropIfNotStatic'");
initialize(
pwd,
!!dropIfExists
ifExists || 'dropIfNotStatic'
).then(() => process.exit(0));

View File

@ -1,4 +1,4 @@
{
"pageTitle": "添加权限",
"pageTitle": "管理权限",
"tips": "请选择模式"
}

View File

@ -1,4 +1,4 @@
{
"pageTitle": "权限管理",
"pageTitle": "修改用户权限",
"tips": "请选择模式"
}

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;

View File

@ -31,6 +31,8 @@ export default function render(props: WebComponentProps<EntityDict, 'user', fals
}
switch (urlAvailable) {
// 现在不在menu里面的页面暂时不检查自己保证console上下文切换时的处理 by Xc
case undefined:
case true: {
return (
<Layout className={Styles.mixPanel}>
@ -89,8 +91,7 @@ export default function render(props: WebComponentProps<EntityDict, 'user', fals
</Layout>
</Layout>
);
}
case undefined: {
} /* {
return (
<Layout className={Styles.mixPanel}>
<Header />
@ -112,6 +113,6 @@ export default function render(props: WebComponentProps<EntityDict, 'user', fals
</Layout>
);
}
} */
}
}